22
33const { API_BASE_PATH } = require ( './client' )
44
5- // Immutable dataset record: { input, expectedOutput?, metadata? }.
5+ // Dataset record: { input, expectedOutput?, metadata?, id? }.
6+ // `id` may be user-provided before push or filled from the backend-created record.
67class DatasetRecord {
7- constructor ( input , expectedOutput = null , metadata = { } ) {
8+ constructor ( input , expectedOutput = null , metadata = { } , id = null ) {
89 this . input = input
910 this . expectedOutput = expectedOutput ?? null
1011 this . metadata = metadata ?? { }
12+ this . id = id ?? null
1113 }
1214}
1315
16+ function createdRecordsFromResponse ( response ) {
17+ if ( Array . isArray ( response ?. records ) ) return response . records
18+ if ( Array . isArray ( response ?. data ) ) return response . data
19+ return [ ]
20+ }
21+
22+ function recordIdFromCreatedRecord ( record ) {
23+ return String ( record ?. id ?? record ?. attributes ?. id ?? '' )
24+ }
25+
26+ function versionFromCreatedRecords ( records ) {
27+ const versions = records
28+ . map ( record => Number ( record ?. attributes ?. valid_from_version ?? record ?. attributes ?. version ) )
29+ . filter ( Number . isFinite )
30+ if ( versions . length === 0 ) return null
31+ return Math . max ( ...versions )
32+ }
33+
1434// A local buffer of dataset records, created remotely and pushed on first run
1535// (or eagerly via push()). Pushes are incremental.
1636class Dataset {
@@ -22,6 +42,8 @@ class Dataset {
2242 #id
2343 #projectId
2444 #pushedCount
45+ #version
46+ #latestVersion
2547
2648 constructor ( client , name , description = '' ) {
2749 this . #client = client
@@ -32,16 +54,20 @@ class Dataset {
3254 this . #id = null
3355 this . #projectId = null
3456 this . #pushedCount = 0
57+ this . #version = null
58+ this . #latestVersion = null
3559 }
3660
3761 // Build a Dataset that already exists remotely (used by pullDataset).
38- static fromExisting ( client , name , description , id , projectId , records , recordIds ) {
62+ static fromExisting ( client , name , description , id , projectId , records , recordIds , version , latestVersion ) {
3963 const dataset = new Dataset ( client , name , description )
4064 dataset . #id = id
4165 dataset . #projectId = projectId
4266 dataset . #records. push ( ...records )
4367 dataset . #recordIds. push ( ...recordIds )
4468 dataset . #pushedCount = records . length
69+ dataset . #version = version ?? null
70+ dataset . #latestVersion = latestVersion ?? version ?? null
4571 return dataset
4672 }
4773
@@ -74,6 +100,14 @@ class Dataset {
74100 return this . #projectId
75101 }
76102
103+ version ( ) {
104+ return this . #version
105+ }
106+
107+ latestVersion ( ) {
108+ return this . #latestVersion
109+ }
110+
77111 // Dashboard URL for this dataset, or null until pushed/pulled.
78112 url ( ) {
79113 if ( this . #id === null ) return null
@@ -100,14 +134,22 @@ class Dataset {
100134 throw new Error ( `Failed to create dataset '${ this . #name} ': ${ err . message } ` )
101135 }
102136 this . #id = response ?. data ?. id ?? null
137+ if ( this . #id === null ) {
138+ throw new Error ( `Failed to create dataset '${ this . #name} ': backend response is missing dataset id` )
139+ }
103140 this . #projectId = projectId
141+ this . #version = response ?. data ?. attributes ?. current_version ?? this . #version
142+ this . #latestVersion = response ?. data ?. attributes ?. current_version ?? this . #latestVersion
104143 }
105144
106145 if ( this . #pushedCount >= this . #records. length ) return { pushedCount : 0 , totalCount : 0 }
107146
108147 const pending = this . #records. slice ( this . #pushedCount)
109148 const records = pending . map ( ( rec ) => {
110149 const out = { input : rec . input }
150+ if ( rec . id != null ) {
151+ out . id = rec . id
152+ }
111153 if ( rec . expectedOutput !== null && rec . expectedOutput !== undefined ) {
112154 out . expected_output = rec . expectedOutput
113155 }
@@ -128,20 +170,30 @@ class Dataset {
128170 throw new Error ( `Failed to push records to dataset '${ this . #name} ': ${ err . message } ` )
129171 }
130172
131- // The append-records response returns created records under a top-level
132- // `records` field, not the usual `data` envelope.
133- const created = response ?. records
173+ // The append-records response has used both a top-level `records` array
174+ // and JSON:API `data` resources. Accept either so generated/custom record
175+ // ids are preserved for experiment row tagging.
176+ const created = createdRecordsFromResponse ( response )
177+ const pushedVersion = versionFromCreatedRecords ( created )
178+ if ( pushedVersion === null ) {
179+ // The dataset contents changed, but the backend did not report the new
180+ // version. Avoid pinning later experiments to the pre-append create version.
181+ this . #version = null
182+ } else {
183+ this . #version = pushedVersion
184+ this . #latestVersion = Math . max ( Number ( this . #latestVersion ?? pushedVersion ) , pushedVersion )
185+ }
186+
134187 let pushedCount = 0
135- if ( Array . isArray ( created ) ) {
136- for ( const node of created ) {
137- const recordId = String ( node ?. id ?? '' )
138- if ( recordId !== '' ) pushedCount ++
139- this . #recordIds . push ( recordId )
188+ for ( const [ index , node ] of created . entries ( ) ) {
189+ const recordId = recordIdFromCreatedRecord ( node )
190+ if ( recordId !== '' ) {
191+ pushedCount ++
192+ pending [ index ] . id = recordId
140193 }
141- for ( let i = created . length ; i < pending . length ; i ++ ) this . #recordIds. push ( '' )
142- } else {
143- for ( let i = 0 ; i < pending . length ; i ++ ) this . #recordIds. push ( '' )
194+ this . #recordIds. push ( recordId )
144195 }
196+ for ( let i = created . length ; i < pending . length ; i ++ ) this . #recordIds. push ( '' )
145197
146198 // Advance by the snapshotted pending count, not the live records length,
147199 // so records added while this push was in flight aren't skipped by the next push.
0 commit comments