@abloatai/ablo 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -3
- package/CHANGELOG.md +20 -0
- package/README.md +3 -3
- package/dist/BaseSyncedStore.d.ts +72 -195
- package/dist/BaseSyncedStore.js +240 -625
- package/dist/Database.d.ts +8 -9
- package/dist/Database.js +9 -14
- package/dist/LazyReferenceCollection.js +5 -0
- package/dist/Model.js +6 -8
- package/dist/ModelRegistry.d.ts +25 -6
- package/dist/ModelRegistry.js +12 -6
- package/dist/NetworkMonitor.js +1 -1
- package/dist/ObjectPool.js +9 -3
- package/dist/SyncClient.d.ts +15 -5
- package/dist/SyncClient.js +152 -66
- package/dist/SyncEngineContext.d.ts +1 -3
- package/dist/SyncEngineContext.js +1 -2
- package/dist/agent/Agent.d.ts +21 -8
- package/dist/agent/Agent.js +26 -13
- package/dist/ai-sdk/coordination-context.js +9 -3
- package/dist/ai-sdk/wrap.d.ts +1 -1
- package/dist/ai-sdk/wrap.js +1 -1
- package/dist/auth/credentialPolicy.d.ts +10 -2
- package/dist/auth/credentialSource.d.ts +1 -13
- package/dist/auth/credentialSource.js +6 -13
- package/dist/auth/index.js +3 -3
- package/dist/cli.cjs +328 -262
- package/dist/client/Ablo.d.ts +35 -764
- package/dist/client/Ablo.js +64 -688
- package/dist/client/ApiClient.d.ts +14 -1
- package/dist/client/ApiClient.js +63 -9
- package/dist/client/auth.d.ts +8 -17
- package/dist/client/auth.js +42 -6
- package/dist/client/consoleLogger.d.ts +36 -0
- package/dist/client/consoleLogger.js +45 -0
- package/dist/client/createInternalComponents.js +1 -1
- package/dist/client/createModelProxy.js +10 -6
- package/dist/client/credentialEndpoint.d.ts +63 -0
- package/dist/client/credentialEndpoint.js +87 -0
- package/dist/client/hostedEndpoints.d.ts +24 -0
- package/dist/client/hostedEndpoints.js +24 -0
- package/dist/client/httpClient.d.ts +1 -1
- package/dist/client/identity.js +2 -1
- package/dist/client/modelRegistration.d.ts +14 -0
- package/dist/client/modelRegistration.js +321 -0
- package/dist/client/options.d.ts +396 -0
- package/dist/client/options.js +10 -0
- package/dist/client/resourceTypes.d.ts +344 -0
- package/dist/client/resourceTypes.js +13 -0
- package/dist/client/schemaConfig.d.ts +56 -0
- package/dist/client/schemaConfig.js +188 -0
- package/dist/client/sessionMint.d.ts +1 -1
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/client/wsMutationExecutor.d.ts +33 -0
- package/dist/client/wsMutationExecutor.js +77 -0
- package/dist/context.js +0 -3
- package/dist/coordination/index.d.ts +8 -1
- package/dist/coordination/index.js +24 -1
- package/dist/coordination/trace.d.ts +0 -1
- package/dist/coordination/trace.js +0 -1
- package/dist/core/DatabaseManager.d.ts +0 -1
- package/dist/core/DatabaseManager.js +26 -24
- package/dist/core/QueryProcessor.js +7 -8
- package/dist/core/QueryView.d.ts +14 -2
- package/dist/core/QueryView.js +4 -5
- package/dist/core/StoreManager.d.ts +2 -2
- package/dist/core/StoreManager.js +5 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/openIDBWithTimeout.js +15 -11
- package/dist/core/query-utils.js +1 -1
- package/dist/core/storeContract.d.ts +143 -0
- package/dist/core/storeContract.js +16 -0
- package/dist/errorCodes.d.ts +11 -2
- package/dist/errorCodes.js +155 -137
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +14 -1
- package/dist/index.js +5 -0
- package/dist/interfaces/index.d.ts +6 -20
- package/dist/keys/index.js +18 -9
- package/dist/mutators/UndoManager.js +6 -1
- package/dist/mutators/defineMutators.d.ts +1 -3
- package/dist/policy/types.d.ts +1 -1
- package/dist/query/client.d.ts +11 -0
- package/dist/query/client.js +81 -52
- package/dist/react/AbloProvider.d.ts +6 -10
- package/dist/react/AbloProvider.js +22 -15
- package/dist/react/context.d.ts +2 -118
- package/dist/react/useAblo.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react/useMutators.js +7 -1
- package/dist/react/useReactive.js +0 -1
- package/dist/react/useUndoScope.js +2 -2
- package/dist/schema/ddlLock.d.ts +39 -0
- package/dist/schema/ddlLock.js +51 -0
- package/dist/schema/field.js +1 -1
- package/dist/schema/index.d.ts +4 -3
- package/dist/schema/index.js +15 -5
- package/dist/schema/model.d.ts +3 -3
- package/dist/schema/model.js +2 -2
- package/dist/schema/openapi.js +5 -1
- package/dist/schema/queries.d.ts +1 -1
- package/dist/schema/queries.js +1 -0
- package/dist/schema/residency.d.ts +35 -0
- package/dist/schema/residency.js +27 -0
- package/dist/schema/roles.d.ts +25 -0
- package/dist/schema/roles.js +37 -0
- package/dist/schema/schema.d.ts +3 -3
- package/dist/schema/schema.js +11 -7
- package/dist/schema/serialize.d.ts +8 -8
- package/dist/schema/sync-delta-row.d.ts +2 -2
- package/dist/schema/sync-delta-row.js +2 -2
- package/dist/server/storage-mode.d.ts +8 -1
- package/dist/server/storage-mode.js +14 -4
- package/dist/source/adapter.d.ts +1 -1
- package/dist/source/adapters/drizzle.js +4 -3
- package/dist/source/adapters/kysely.js +4 -3
- package/dist/source/adapters/memory.js +1 -1
- package/dist/source/adapters/prisma.js +5 -4
- package/dist/source/conformance.js +10 -8
- package/dist/source/connector.js +4 -2
- package/dist/source/factory.d.ts +106 -0
- package/dist/source/factory.js +273 -0
- package/dist/source/index.d.ts +11 -461
- package/dist/source/index.js +14 -416
- package/dist/source/next.d.ts +1 -1
- package/dist/source/next.js +1 -1
- package/dist/source/pushQueue.d.ts +1 -1
- package/dist/source/pushQueue.js +6 -3
- package/dist/source/signing.d.ts +59 -0
- package/dist/source/signing.js +142 -0
- package/dist/source/types.d.ts +334 -0
- package/dist/source/types.js +44 -0
- package/dist/stores/ObjectStore.d.ts +1 -1
- package/dist/stores/ObjectStore.js +23 -23
- package/dist/stores/SyncActionStore.d.ts +1 -1
- package/dist/stores/SyncActionStore.js +70 -35
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +1 -0
- package/dist/sync/AreaOfInterestManager.d.ts +2 -2
- package/dist/sync/AreaOfInterestManager.js +1 -1
- package/dist/sync/BootstrapHelper.d.ts +1 -3
- package/dist/sync/BootstrapHelper.js +29 -8
- package/dist/sync/ConnectionManager.d.ts +8 -0
- package/dist/sync/ConnectionManager.js +19 -4
- package/dist/sync/HydrationCoordinator.d.ts +12 -0
- package/dist/sync/HydrationCoordinator.js +25 -5
- package/dist/sync/NetworkProbe.d.ts +3 -2
- package/dist/sync/NetworkProbe.js +7 -6
- package/dist/sync/SyncWebSocket.d.ts +71 -120
- package/dist/sync/SyncWebSocket.js +324 -630
- package/dist/sync/awaitClaimGrant.js +27 -19
- package/dist/sync/bootstrapApply.d.ts +60 -0
- package/dist/sync/bootstrapApply.js +65 -0
- package/dist/sync/commitFrames.d.ts +43 -0
- package/dist/sync/commitFrames.js +94 -0
- package/dist/sync/createClaimStream.js +3 -2
- package/dist/sync/createSnapshot.d.ts +1 -3
- package/dist/sync/credentialLifecycle.d.ts +175 -0
- package/dist/sync/credentialLifecycle.js +329 -0
- package/dist/sync/deltaPipeline.d.ts +111 -0
- package/dist/sync/deltaPipeline.js +262 -0
- package/dist/sync/groupChange.d.ts +111 -0
- package/dist/sync/groupChange.js +246 -0
- package/dist/sync/heartbeat.d.ts +62 -0
- package/dist/sync/heartbeat.js +91 -0
- package/dist/sync/participants.d.ts +8 -8
- package/dist/sync/syncCursor.d.ts +44 -0
- package/dist/sync/syncCursor.js +59 -0
- package/dist/sync/syncPlan.d.ts +62 -0
- package/dist/sync/syncPlan.js +51 -0
- package/dist/sync/wsFrameHandlers.d.ts +117 -0
- package/dist/sync/wsFrameHandlers.js +378 -0
- package/dist/testing/fixtures/bootstrap.d.ts +9 -11
- package/dist/testing/fixtures/deltas.d.ts +1 -1
- package/dist/testing/fixtures/models.js +1 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +0 -3
- package/dist/testing/helpers/sync-engine-harness.js +2 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/mocks/MockSyncContext.d.ts +1 -17
- package/dist/testing/mocks/MockSyncContext.js +1 -32
- package/dist/testing/mocks/MockSyncStore.d.ts +4 -4
- package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
- package/dist/transactions/TransactionQueue.d.ts +20 -56
- package/dist/transactions/TransactionQueue.js +101 -487
- package/dist/transactions/TransactionStore.d.ts +18 -0
- package/dist/transactions/TransactionStore.js +51 -0
- package/dist/transactions/coalesceRules.d.ts +34 -0
- package/dist/transactions/coalesceRules.js +117 -0
- package/dist/transactions/commitPayload.d.ts +134 -0
- package/dist/transactions/commitPayload.js +152 -0
- package/dist/transactions/deltaConfirmation.d.ts +60 -0
- package/dist/transactions/deltaConfirmation.js +223 -0
- package/dist/transactions/optimistic.d.ts +24 -0
- package/dist/transactions/optimistic.js +45 -0
- package/dist/transactions/persistedReplay.d.ts +93 -0
- package/dist/transactions/persistedReplay.js +105 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js +0 -5
- package/dist/types/modelData.d.ts +12 -0
- package/dist/types/modelData.js +11 -0
- package/dist/types/participant.d.ts +21 -0
- package/dist/types/participant.js +12 -0
- package/dist/types/streams.d.ts +8 -14
- package/dist/utils/mobx-setup.d.ts +1 -1
- package/dist/utils/mobx-setup.js +2 -2
- package/dist/wire/errorEnvelope.d.ts +11 -2
- package/dist/wire/errorEnvelope.js +14 -2
- package/dist/wire/frames.d.ts +90 -0
- package/dist/wire/frames.js +55 -1
- package/dist/wire/index.d.ts +4 -1
- package/dist/wire/index.js +20 -1
- package/dist/wire/protocol.d.ts +45 -0
- package/dist/wire/protocol.js +45 -0
- package/dist/wire/protocolVersion.d.ts +56 -0
- package/dist/wire/protocolVersion.js +63 -0
- package/docs/api-keys.md +4 -3
- package/docs/examples/existing-python-backend.md +3 -3
- package/docs/identity.md +4 -4
- package/docs/integration-guide.md +1 -1
- package/docs/react.md +1 -1
- package/docs/sessions.md +5 -7
- package/package.json +15 -13
- package/dist/client/index.d.ts +0 -36
- package/dist/client/index.js +0 -33
- package/dist/config/index.d.ts +0 -10
- package/dist/config/index.js +0 -12
- package/dist/interfaces/headless.d.ts +0 -95
- package/dist/interfaces/headless.js +0 -41
- package/dist/query/index.d.ts +0 -6
- package/dist/query/index.js +0 -5
- package/dist/realtime/index.d.ts +0 -10
- package/dist/realtime/index.js +0 -9
- package/dist/schema/plane.d.ts +0 -23
- package/dist/schema/plane.js +0 -19
- package/dist/server/next.d.ts +0 -51
- package/dist/server/next.js +0 -47
- package/dist/sync/OfflineFlush.d.ts +0 -9
- package/dist/sync/OfflineFlush.js +0 -22
- package/dist/sync/OfflineTransactionStore.d.ts +0 -37
- package/dist/sync/OfflineTransactionStore.js +0 -263
- package/dist/transactions/index.d.ts +0 -16
- package/dist/transactions/index.js +0 -7
|
@@ -4,6 +4,36 @@
|
|
|
4
4
|
* Stores and manages sync actions received from the server.
|
|
5
5
|
* Critical for delta sync and maintaining sync state consistency.
|
|
6
6
|
*/
|
|
7
|
+
// Uses native IndexedDB for maximum performance
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { getContext } from '../context.js';
|
|
10
|
+
/**
|
|
11
|
+
* Zod boundary for rows read back from the sync-action store (T1.8): they
|
|
12
|
+
* were written by a previous session, so their shape is not guaranteed by
|
|
13
|
+
* this build. Default (strip) mode drops the storage bookkeeping fields
|
|
14
|
+
* (`storedAt`/`applied`/`appliedAt`) that used to be peeled off with an
|
|
15
|
+
* untyped rest-spread. Rows that don't parse are dropped + logged instead
|
|
16
|
+
* of being replayed as malformed deltas.
|
|
17
|
+
*/
|
|
18
|
+
const storedSyncActionSchema = z.object({
|
|
19
|
+
id: z.number(),
|
|
20
|
+
modelName: z.string(),
|
|
21
|
+
modelId: z.string(),
|
|
22
|
+
action: z.enum(['I', 'U', 'A', 'D', 'C', 'G', 'S', 'V']),
|
|
23
|
+
data: z.unknown(),
|
|
24
|
+
__class: z.literal('SyncAction').default('SyncAction'),
|
|
25
|
+
});
|
|
26
|
+
/** Parse one stored row into a SyncAction, or `null` (dropped + logged). */
|
|
27
|
+
function toSyncAction(row) {
|
|
28
|
+
const parsed = storedSyncActionSchema.safeParse(row);
|
|
29
|
+
if (!parsed.success) {
|
|
30
|
+
getContext().logger.debug('[SyncActionStore] Dropping malformed stored sync action', {
|
|
31
|
+
issues: parsed.error.issues.map((i) => i.path.join('.')).join(', '),
|
|
32
|
+
});
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return parsed.data;
|
|
36
|
+
}
|
|
7
37
|
/**
|
|
8
38
|
* SyncActionStore - Manages sync actions (deltas)
|
|
9
39
|
*
|
|
@@ -51,8 +81,8 @@ export class SyncActionStore {
|
|
|
51
81
|
}
|
|
52
82
|
resolve();
|
|
53
83
|
};
|
|
54
|
-
tx.onerror = () => reject(tx.error);
|
|
55
|
-
request.onerror = () => reject(request.error);
|
|
84
|
+
tx.onerror = () => { reject(tx.error); };
|
|
85
|
+
request.onerror = () => { reject(request.error); };
|
|
56
86
|
});
|
|
57
87
|
}
|
|
58
88
|
/**
|
|
@@ -83,10 +113,10 @@ export class SyncActionStore {
|
|
|
83
113
|
}
|
|
84
114
|
}
|
|
85
115
|
};
|
|
86
|
-
request.onerror = () => reject(request.error);
|
|
116
|
+
request.onerror = () => { reject(request.error); };
|
|
87
117
|
}
|
|
88
|
-
tx.oncomplete = () => resolve();
|
|
89
|
-
tx.onerror = () => reject(tx.error);
|
|
118
|
+
tx.oncomplete = () => { resolve(); };
|
|
119
|
+
tx.onerror = () => { reject(tx.error); };
|
|
90
120
|
});
|
|
91
121
|
}
|
|
92
122
|
/**
|
|
@@ -103,10 +133,9 @@ export class SyncActionStore {
|
|
|
103
133
|
resolve(undefined);
|
|
104
134
|
return;
|
|
105
135
|
}
|
|
106
|
-
|
|
107
|
-
resolve(action);
|
|
136
|
+
resolve(toSyncAction(data) ?? undefined);
|
|
108
137
|
};
|
|
109
|
-
request.onerror = () => reject(request.error);
|
|
138
|
+
request.onerror = () => { reject(request.error); };
|
|
110
139
|
});
|
|
111
140
|
}
|
|
112
141
|
/**
|
|
@@ -121,10 +150,12 @@ export class SyncActionStore {
|
|
|
121
150
|
const request = index.getAll(range);
|
|
122
151
|
request.onsuccess = () => {
|
|
123
152
|
const allData = request.result;
|
|
124
|
-
const actions = allData
|
|
153
|
+
const actions = allData
|
|
154
|
+
.map(toSyncAction)
|
|
155
|
+
.filter((action) => action !== null);
|
|
125
156
|
resolve(actions);
|
|
126
157
|
};
|
|
127
|
-
request.onerror = () => reject(request.error);
|
|
158
|
+
request.onerror = () => { reject(request.error); };
|
|
128
159
|
});
|
|
129
160
|
}
|
|
130
161
|
/**
|
|
@@ -142,7 +173,9 @@ export class SyncActionStore {
|
|
|
142
173
|
* Mark sync action as applied
|
|
143
174
|
*/
|
|
144
175
|
async markAsApplied(syncId) {
|
|
145
|
-
|
|
176
|
+
// Plain (non-async) executor: a sync throw (e.g. transaction() on a
|
|
177
|
+
// closing DB) must reject this promise, not vanish into an ignored one.
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
146
179
|
const tx = this.db.transaction([this.storeName], 'readwrite');
|
|
147
180
|
const store = tx.objectStore(this.storeName);
|
|
148
181
|
const getRequest = store.get(syncId);
|
|
@@ -152,10 +185,10 @@ export class SyncActionStore {
|
|
|
152
185
|
existing.applied = true;
|
|
153
186
|
existing.appliedAt = Date.now();
|
|
154
187
|
const putRequest = store.put(existing);
|
|
155
|
-
putRequest.onerror = () => reject(putRequest.error);
|
|
188
|
+
putRequest.onerror = () => { reject(putRequest.error); };
|
|
156
189
|
}
|
|
157
190
|
};
|
|
158
|
-
getRequest.onerror = () => reject(getRequest.error);
|
|
191
|
+
getRequest.onerror = () => { reject(getRequest.error); };
|
|
159
192
|
tx.oncomplete = async () => {
|
|
160
193
|
// Update tracking
|
|
161
194
|
if (syncId > this.lastAppliedSyncId) {
|
|
@@ -172,7 +205,7 @@ export class SyncActionStore {
|
|
|
172
205
|
reject(error);
|
|
173
206
|
}
|
|
174
207
|
};
|
|
175
|
-
tx.onerror = () => reject(tx.error);
|
|
208
|
+
tx.onerror = () => { reject(tx.error); };
|
|
176
209
|
});
|
|
177
210
|
}
|
|
178
211
|
/**
|
|
@@ -181,7 +214,7 @@ export class SyncActionStore {
|
|
|
181
214
|
async markManyAsApplied(syncIds) {
|
|
182
215
|
if (syncIds.length === 0)
|
|
183
216
|
return;
|
|
184
|
-
return new Promise(
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
185
218
|
const tx = this.db.transaction([this.storeName], 'readwrite');
|
|
186
219
|
const store = tx.objectStore(this.storeName);
|
|
187
220
|
let processed = 0;
|
|
@@ -206,14 +239,14 @@ export class SyncActionStore {
|
|
|
206
239
|
}
|
|
207
240
|
}
|
|
208
241
|
};
|
|
209
|
-
putRequest.onerror = () => reject(putRequest.error);
|
|
242
|
+
putRequest.onerror = () => { reject(putRequest.error); };
|
|
210
243
|
}
|
|
211
244
|
else {
|
|
212
245
|
processed++;
|
|
213
246
|
this.pendingActions.delete(syncId);
|
|
214
247
|
}
|
|
215
248
|
};
|
|
216
|
-
getRequest.onerror = () => reject(getRequest.error);
|
|
249
|
+
getRequest.onerror = () => { reject(getRequest.error); };
|
|
217
250
|
}
|
|
218
251
|
tx.oncomplete = async () => {
|
|
219
252
|
// Update metadata with the highest sync ID
|
|
@@ -231,7 +264,7 @@ export class SyncActionStore {
|
|
|
231
264
|
resolve();
|
|
232
265
|
}
|
|
233
266
|
};
|
|
234
|
-
tx.onerror = () => reject(tx.error);
|
|
267
|
+
tx.onerror = () => { reject(tx.error); };
|
|
235
268
|
});
|
|
236
269
|
}
|
|
237
270
|
/**
|
|
@@ -265,7 +298,7 @@ export class SyncActionStore {
|
|
|
265
298
|
currentId++;
|
|
266
299
|
checkNext();
|
|
267
300
|
};
|
|
268
|
-
request.onerror = () => reject(request.error);
|
|
301
|
+
request.onerror = () => { reject(request.error); };
|
|
269
302
|
};
|
|
270
303
|
checkNext();
|
|
271
304
|
});
|
|
@@ -293,7 +326,7 @@ export class SyncActionStore {
|
|
|
293
326
|
currentId++;
|
|
294
327
|
checkNext();
|
|
295
328
|
};
|
|
296
|
-
request.onerror = () => reject(request.error);
|
|
329
|
+
request.onerror = () => { reject(request.error); };
|
|
297
330
|
};
|
|
298
331
|
checkNext();
|
|
299
332
|
});
|
|
@@ -317,7 +350,7 @@ export class SyncActionStore {
|
|
|
317
350
|
cleaned++;
|
|
318
351
|
cursor.continue();
|
|
319
352
|
};
|
|
320
|
-
deleteRequest.onerror = () => reject(deleteRequest.error);
|
|
353
|
+
deleteRequest.onerror = () => { reject(deleteRequest.error); };
|
|
321
354
|
}
|
|
322
355
|
else {
|
|
323
356
|
cursor.continue();
|
|
@@ -328,7 +361,7 @@ export class SyncActionStore {
|
|
|
328
361
|
resolve(cleaned);
|
|
329
362
|
}
|
|
330
363
|
};
|
|
331
|
-
request.onerror = () => reject(request.error);
|
|
364
|
+
request.onerror = () => { reject(request.error); };
|
|
332
365
|
});
|
|
333
366
|
}
|
|
334
367
|
/**
|
|
@@ -344,8 +377,8 @@ export class SyncActionStore {
|
|
|
344
377
|
this.lastAppliedSyncId = 0;
|
|
345
378
|
resolve();
|
|
346
379
|
};
|
|
347
|
-
tx.onerror = () => reject(tx.error);
|
|
348
|
-
request.onerror = () => reject(request.error);
|
|
380
|
+
tx.onerror = () => { reject(tx.error); };
|
|
381
|
+
request.onerror = () => { reject(request.error); };
|
|
349
382
|
});
|
|
350
383
|
}
|
|
351
384
|
/**
|
|
@@ -373,7 +406,7 @@ export class SyncActionStore {
|
|
|
373
406
|
newestAction: timestamps.length > 0 ? new Date(timestamps[timestamps.length - 1]) : null,
|
|
374
407
|
});
|
|
375
408
|
};
|
|
376
|
-
request.onerror = () => reject(request.error);
|
|
409
|
+
request.onerror = () => { reject(request.error); };
|
|
377
410
|
});
|
|
378
411
|
}
|
|
379
412
|
/**
|
|
@@ -389,11 +422,11 @@ export class SyncActionStore {
|
|
|
389
422
|
metadata.lastSyncId = syncId;
|
|
390
423
|
metadata.updatedAt = new Date();
|
|
391
424
|
const putRequest = store.put(metadata, 'metadata');
|
|
392
|
-
putRequest.onerror = () => reject(putRequest.error);
|
|
425
|
+
putRequest.onerror = () => { reject(putRequest.error); };
|
|
393
426
|
};
|
|
394
|
-
getRequest.onerror = () => reject(getRequest.error);
|
|
395
|
-
tx.oncomplete = () => resolve();
|
|
396
|
-
tx.onerror = () => reject(tx.error);
|
|
427
|
+
getRequest.onerror = () => { reject(getRequest.error); };
|
|
428
|
+
tx.oncomplete = () => { resolve(); };
|
|
429
|
+
tx.onerror = () => { reject(tx.error); };
|
|
397
430
|
});
|
|
398
431
|
}
|
|
399
432
|
/**
|
|
@@ -416,14 +449,14 @@ export class SyncActionStore {
|
|
|
416
449
|
resolve(undefined);
|
|
417
450
|
}
|
|
418
451
|
};
|
|
419
|
-
request.onerror = () => reject(request.error);
|
|
452
|
+
request.onerror = () => { reject(request.error); };
|
|
420
453
|
});
|
|
421
454
|
}
|
|
422
455
|
/**
|
|
423
456
|
* Rewind to a specific sync ID (for conflict resolution)
|
|
424
457
|
*/
|
|
425
458
|
async rewindTo(syncId) {
|
|
426
|
-
return new Promise(
|
|
459
|
+
return new Promise((resolve, reject) => {
|
|
427
460
|
// Get all actions after this sync ID
|
|
428
461
|
const tx = this.db.transaction([this.storeName], 'readonly');
|
|
429
462
|
const store = tx.objectStore(this.storeName);
|
|
@@ -461,21 +494,23 @@ export class SyncActionStore {
|
|
|
461
494
|
this.lastAppliedSyncId = syncId - 1;
|
|
462
495
|
}
|
|
463
496
|
};
|
|
464
|
-
putRequest.onerror = () => reject(putRequest.error);
|
|
497
|
+
putRequest.onerror = () => { reject(putRequest.error); };
|
|
465
498
|
}
|
|
466
499
|
writeTx.oncomplete = async () => {
|
|
467
500
|
try {
|
|
468
501
|
await this.updateLastSyncId(this.lastAppliedSyncId);
|
|
469
|
-
const result = actionsToRewind
|
|
502
|
+
const result = actionsToRewind
|
|
503
|
+
.map(toSyncAction)
|
|
504
|
+
.filter((action) => action !== null);
|
|
470
505
|
resolve(result);
|
|
471
506
|
}
|
|
472
507
|
catch (error) {
|
|
473
508
|
reject(error);
|
|
474
509
|
}
|
|
475
510
|
};
|
|
476
|
-
writeTx.onerror = () => reject(writeTx.error);
|
|
511
|
+
writeTx.onerror = () => { reject(writeTx.error); };
|
|
477
512
|
};
|
|
478
|
-
request.onerror = () => reject(request.error);
|
|
513
|
+
request.onerror = () => { reject(request.error); };
|
|
479
514
|
});
|
|
480
515
|
}
|
|
481
516
|
}
|
package/dist/surface.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare const PUBLIC_MODEL_VERBS: readonly ["retrieve", "list", "get", "g
|
|
|
23
23
|
export declare const PUBLIC_LIST_OPTION_KEYS: readonly ["where", "filter", "orderBy", "limit", "offset", "state"];
|
|
24
24
|
/** Public keys of `AbloOptions`. `schema` is required; the rest are optional
|
|
25
25
|
* (the locked happy path is `Ablo({ schema, apiKey, databaseUrl, transport })`). */
|
|
26
|
-
export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ["schema", "apiKey", "databaseUrl", "persistence", "transport", "debug", "logLevel", "authToken", "baseURL", "fetch", "defaultHeaders", "defaultQuery", "dangerouslyAllowBrowser"];
|
|
26
|
+
export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ["schema", "apiKey", "authEndpoint", "databaseUrl", "persistence", "transport", "debug", "logLevel", "authToken", "baseURL", "fetch", "defaultHeaders", "defaultQuery", "dangerouslyAllowBrowser"];
|
|
27
27
|
export type ModelVerb = (typeof PUBLIC_MODEL_VERBS)[number];
|
|
28
28
|
export type ListOptionKey = (typeof PUBLIC_LIST_OPTION_KEYS)[number];
|
|
29
29
|
export type AbloOptionKey = (typeof PUBLIC_ABLO_OPTION_KEYS)[number];
|
package/dist/surface.js
CHANGED
|
@@ -41,7 +41,7 @@ export interface SubscriptionTransport {
|
|
|
41
41
|
* Resolves with the server's effective set (which the manager treats as
|
|
42
42
|
* authoritative for its next diff).
|
|
43
43
|
*/
|
|
44
|
-
updateSubscription(syncGroups:
|
|
44
|
+
updateSubscription(syncGroups: readonly string[]): Promise<{
|
|
45
45
|
syncGroups: string[];
|
|
46
46
|
}>;
|
|
47
47
|
}
|
|
@@ -52,7 +52,7 @@ export interface AreaOfInterestOptions {
|
|
|
52
52
|
* Groups always present in the effective set (e.g. `org:<id>`,
|
|
53
53
|
* `user:<id>`). Never warm, never expired.
|
|
54
54
|
*/
|
|
55
|
-
baseGroups?:
|
|
55
|
+
baseGroups?: readonly string[];
|
|
56
56
|
/**
|
|
57
57
|
* How long a `leave`-ed group stays subscribed before it actually drops.
|
|
58
58
|
* This is the hysteresis margin. Default 30s.
|
|
@@ -71,7 +71,7 @@ export class AreaOfInterestManager {
|
|
|
71
71
|
const schedule = options.scheduler ??
|
|
72
72
|
((fn, ms) => {
|
|
73
73
|
const handle = setInterval(fn, ms);
|
|
74
|
-
return () => clearInterval(handle);
|
|
74
|
+
return () => { clearInterval(handle); };
|
|
75
75
|
});
|
|
76
76
|
this.cancelSweep = schedule(() => {
|
|
77
77
|
void this.sweep();
|
|
@@ -10,9 +10,7 @@ export interface BootstrapData {
|
|
|
10
10
|
* boundary — the per-model shape is asserted by the consumer (sync
|
|
11
11
|
* engine reduce + IDB write) using the registered schema.
|
|
12
12
|
*/
|
|
13
|
-
models?:
|
|
14
|
-
[typename: string]: unknown[];
|
|
15
|
-
};
|
|
13
|
+
models?: Record<string, unknown[]>;
|
|
16
14
|
deltas?: ValidatedServerDelta[];
|
|
17
15
|
deltaCount?: number;
|
|
18
16
|
/** Model types whose server-side query failed (timeout, RLS error, etc.) */
|
|
@@ -176,7 +176,7 @@ export class BootstrapHelper {
|
|
|
176
176
|
// Also do NOT fallback to cache - the user must sign in again
|
|
177
177
|
if (SyncSessionError.isSessionError(error)) {
|
|
178
178
|
getContext().observability.breadcrumb('Bootstrap session error - redirecting to sign-in', 'sync.bootstrap', 'warning', {
|
|
179
|
-
statusCode: error.statusCode,
|
|
179
|
+
statusCode: (error).statusCode,
|
|
180
180
|
});
|
|
181
181
|
throw error;
|
|
182
182
|
}
|
|
@@ -226,7 +226,7 @@ export class BootstrapHelper {
|
|
|
226
226
|
// client-side was historical: it predated the auth-context pipeline
|
|
227
227
|
// and forced a cross-org guard to defend against the SDK lying.
|
|
228
228
|
const params = new URLSearchParams();
|
|
229
|
-
this.options.syncGroups.forEach((g) => params.append('syncGroups', g));
|
|
229
|
+
this.options.syncGroups.forEach((g) => { params.append('syncGroups', g); });
|
|
230
230
|
if (this.options.instantModels && this.options.instantModels.length > 0) {
|
|
231
231
|
params.append('models', this.options.instantModels.join(','));
|
|
232
232
|
}
|
|
@@ -369,12 +369,33 @@ export class BootstrapHelper {
|
|
|
369
369
|
*/
|
|
370
370
|
async fetchEntity(modelName, id) {
|
|
371
371
|
const url = `${this.options.baseUrl}/sync/entity/${modelName}/${id}`;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
});
|
|
372
|
+
// Same `fetchTimeout` deadline `performFetch` uses — this was the one
|
|
373
|
+
// fetch in this file with no AbortSignal, so a hung self-heal read could
|
|
374
|
+
// stall its caller forever. A LOCAL controller (not `this.abortController`)
|
|
375
|
+
// so an entity self-heal never cancels a concurrent bootstrap fetch.
|
|
376
|
+
const controller = new AbortController();
|
|
377
|
+
const timeoutId = setTimeout(() => { controller.abort(); }, this.options.fetchTimeout);
|
|
378
|
+
let response;
|
|
379
|
+
try {
|
|
380
|
+
response = await fetch(url, {
|
|
381
|
+
method: 'GET',
|
|
382
|
+
headers: withAuthHeaders(this.options.getAuthToken, {
|
|
383
|
+
'Content-Type': 'application/json',
|
|
384
|
+
}, this.options.authToken),
|
|
385
|
+
signal: controller.signal,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
// Convert abort to the existing typed timeout error (same code as the
|
|
390
|
+
// bootstrap fetch path) so callers get a retryable connection error.
|
|
391
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
392
|
+
throw new AbloConnectionError(`Entity fetch timed out after ${this.options.fetchTimeout}ms`, { code: 'bootstrap_fetch_timeout', cause: error });
|
|
393
|
+
}
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
clearTimeout(timeoutId);
|
|
398
|
+
}
|
|
378
399
|
if (response.status === 404) {
|
|
379
400
|
return null;
|
|
380
401
|
}
|
|
@@ -167,6 +167,14 @@ export declare class ConnectionManager {
|
|
|
167
167
|
send(event: ConnectionEvent): void;
|
|
168
168
|
private transition;
|
|
169
169
|
private onEnterState;
|
|
170
|
+
/**
|
|
171
|
+
* FSM effects are fire-and-forget by design (`onEnterState` is sync). Each
|
|
172
|
+
* effect catches its own operational failures and maps them onto an FSM
|
|
173
|
+
* event, so a rejection here means the machine wedged mid-transition
|
|
174
|
+
* (`send()` or a transition listener threw) with no retry timer armed past
|
|
175
|
+
* it — capture it instead of losing the reconnect loop silently.
|
|
176
|
+
*/
|
|
177
|
+
private captureEffectFailure;
|
|
170
178
|
private runProbe;
|
|
171
179
|
/**
|
|
172
180
|
* Re-mint the short-lived access key on `refreshing_credential`. Delegates to
|
|
@@ -349,16 +349,16 @@ export class ConnectionManager {
|
|
|
349
349
|
if (event.type === 'WS_DISCONNECTED') {
|
|
350
350
|
this.callbacks?.onDisconnectWebSocket();
|
|
351
351
|
}
|
|
352
|
-
this.runProbe();
|
|
352
|
+
void this.runProbe().catch(this.captureEffectFailure('probe'));
|
|
353
353
|
break;
|
|
354
354
|
case 'waiting_for_network':
|
|
355
355
|
this.scheduleBackoff();
|
|
356
356
|
break;
|
|
357
357
|
case 'reconnecting':
|
|
358
|
-
this.runReconnect();
|
|
358
|
+
void this.runReconnect().catch(this.captureEffectFailure('reconnect'));
|
|
359
359
|
break;
|
|
360
360
|
case 'refreshing_credential':
|
|
361
|
-
this.runRefreshCredential();
|
|
361
|
+
void this.runRefreshCredential().catch(this.captureEffectFailure('refresh-credential'));
|
|
362
362
|
break;
|
|
363
363
|
case 'backoff':
|
|
364
364
|
this.scheduleBackoff();
|
|
@@ -382,6 +382,21 @@ export class ConnectionManager {
|
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
384
|
// ── Async operations ─────────────────────────────────────────────────
|
|
385
|
+
/**
|
|
386
|
+
* FSM effects are fire-and-forget by design (`onEnterState` is sync). Each
|
|
387
|
+
* effect catches its own operational failures and maps them onto an FSM
|
|
388
|
+
* event, so a rejection here means the machine wedged mid-transition
|
|
389
|
+
* (`send()` or a transition listener threw) with no retry timer armed past
|
|
390
|
+
* it — capture it instead of losing the reconnect loop silently.
|
|
391
|
+
*/
|
|
392
|
+
captureEffectFailure(effect) {
|
|
393
|
+
return (error) => {
|
|
394
|
+
getContext().observability.captureWebSocketError({
|
|
395
|
+
context: `connection-fsm-${effect}`,
|
|
396
|
+
error: error instanceof Error ? error.message : String(error),
|
|
397
|
+
});
|
|
398
|
+
};
|
|
399
|
+
}
|
|
385
400
|
async runProbe() {
|
|
386
401
|
try {
|
|
387
402
|
const result = await probeNetwork({
|
|
@@ -609,7 +624,7 @@ export class ConnectionManager {
|
|
|
609
624
|
// means definitely offline"), and reloading while truly offline
|
|
610
625
|
// would either serve cached content forever or fail and leave
|
|
611
626
|
// the user with a broken page.
|
|
612
|
-
const browserOnline = typeof navigator === 'undefined' || navigator.onLine
|
|
627
|
+
const browserOnline = typeof navigator === 'undefined' || navigator.onLine;
|
|
613
628
|
if (this.stuckCycles >= MAX_STUCK_CYCLES_BEFORE_RELOAD && browserOnline) {
|
|
614
629
|
getContext().logger.debug('[ConnectionManager] Watchdog: sustained stuck — hard reload');
|
|
615
630
|
getContext().observability.breadcrumb('Watchdog hard reload', 'sync.offline', 'error');
|
|
@@ -23,6 +23,7 @@ import type { ObjectPool } from '../ObjectPool.js';
|
|
|
23
23
|
import type { Database } from '../Database.js';
|
|
24
24
|
import type { Model } from '../Model.js';
|
|
25
25
|
import type { ModelRegistry } from '../ModelRegistry.js';
|
|
26
|
+
import type { RecoveryClass } from '../errorCodes.js';
|
|
26
27
|
import type { LoadWhere, WhereClause } from '../query/types.js';
|
|
27
28
|
import type { Schema } from '../schema/schema.js';
|
|
28
29
|
export interface HydrationCoordinatorOptions {
|
|
@@ -94,6 +95,15 @@ export declare class HydrationCoordinator {
|
|
|
94
95
|
*/
|
|
95
96
|
private readonly hydratedKeys;
|
|
96
97
|
private authTokenProvider;
|
|
98
|
+
/**
|
|
99
|
+
* The auth-recovery backbone (the store's `recoverFromAuthRejection`),
|
|
100
|
+
* late-bound like {@link setAuthTokenProvider} because the store doesn't
|
|
101
|
+
* exist yet when the coordinator is constructed. Handed to `postQuery` so a
|
|
102
|
+
* 401 on the lazy lane re-mints through the SAME single-flight path the WS
|
|
103
|
+
* probe and proactive pre-roll use, then replays once — instead of silently
|
|
104
|
+
* returning empty rows against an expired key forever.
|
|
105
|
+
*/
|
|
106
|
+
private credentialRecovery;
|
|
97
107
|
constructor(opts: HydrationCoordinatorOptions);
|
|
98
108
|
/**
|
|
99
109
|
* Late-bind the auth token getter. Browser cookie consumers can omit this;
|
|
@@ -101,6 +111,8 @@ export declare class HydrationCoordinator {
|
|
|
101
111
|
* bootstrap and the WebSocket.
|
|
102
112
|
*/
|
|
103
113
|
setAuthTokenProvider(provider: () => string | null): void;
|
|
114
|
+
/** Late-bind the auth-recovery backbone. See {@link credentialRecovery}. */
|
|
115
|
+
setCredentialRecovery(recover: (recovery: RecoveryClass) => Promise<'retry' | 'stop'>): void;
|
|
104
116
|
/** @deprecated Use `setAuthTokenProvider`. */
|
|
105
117
|
setCapabilityTokenProvider(provider: () => string | null): void;
|
|
106
118
|
/**
|
|
@@ -45,6 +45,15 @@ export class HydrationCoordinator {
|
|
|
45
45
|
*/
|
|
46
46
|
hydratedKeys = new Set();
|
|
47
47
|
authTokenProvider = null;
|
|
48
|
+
/**
|
|
49
|
+
* The auth-recovery backbone (the store's `recoverFromAuthRejection`),
|
|
50
|
+
* late-bound like {@link setAuthTokenProvider} because the store doesn't
|
|
51
|
+
* exist yet when the coordinator is constructed. Handed to `postQuery` so a
|
|
52
|
+
* 401 on the lazy lane re-mints through the SAME single-flight path the WS
|
|
53
|
+
* probe and proactive pre-roll use, then replays once — instead of silently
|
|
54
|
+
* returning empty rows against an expired key forever.
|
|
55
|
+
*/
|
|
56
|
+
credentialRecovery = null;
|
|
48
57
|
constructor(opts) {
|
|
49
58
|
this.opts = opts;
|
|
50
59
|
this.authTokenProvider = opts.getAuthToken ?? opts.getCapabilityToken ?? null;
|
|
@@ -57,6 +66,10 @@ export class HydrationCoordinator {
|
|
|
57
66
|
setAuthTokenProvider(provider) {
|
|
58
67
|
this.authTokenProvider = provider;
|
|
59
68
|
}
|
|
69
|
+
/** Late-bind the auth-recovery backbone. See {@link credentialRecovery}. */
|
|
70
|
+
setCredentialRecovery(recover) {
|
|
71
|
+
this.credentialRecovery = recover;
|
|
72
|
+
}
|
|
60
73
|
/** @deprecated Use `setAuthTokenProvider`. */
|
|
61
74
|
setCapabilityTokenProvider(provider) {
|
|
62
75
|
this.setAuthTokenProvider(provider);
|
|
@@ -82,9 +95,15 @@ export class HydrationCoordinator {
|
|
|
82
95
|
return inFlight;
|
|
83
96
|
const work = this.runFetch(modelName, typename, ModelClass, clauses, options, queryKey);
|
|
84
97
|
this.inFlight.set(queryKey, work);
|
|
85
|
-
|
|
98
|
+
// The rejection (if any) reaches callers via the returned `work`; this
|
|
99
|
+
// side-chain only clears the single-flight slot. Without the trailing
|
|
100
|
+
// catch, `.finally()` mirrors the rejection into a second, unhandled
|
|
101
|
+
// promise even when every caller handles theirs.
|
|
102
|
+
void work
|
|
103
|
+
.finally(() => {
|
|
86
104
|
this.inFlight.delete(queryKey);
|
|
87
|
-
})
|
|
105
|
+
})
|
|
106
|
+
.catch(() => undefined);
|
|
88
107
|
return work;
|
|
89
108
|
}
|
|
90
109
|
async runFetch(modelName, typename, ModelClass, clauses, options, queryKey) {
|
|
@@ -282,7 +301,7 @@ export class HydrationCoordinator {
|
|
|
282
301
|
if (!Array.isArray(all))
|
|
283
302
|
return [];
|
|
284
303
|
const idSet = new Set(parentIds);
|
|
285
|
-
return all.filter((r) => idSet.has(r[foreignKey]));
|
|
304
|
+
return all.filter((r) => idSet.has((r)[foreignKey]));
|
|
286
305
|
}
|
|
287
306
|
catch {
|
|
288
307
|
return [];
|
|
@@ -368,6 +387,7 @@ export class HydrationCoordinator {
|
|
|
368
387
|
const result = await postQuery({
|
|
369
388
|
baseUrl: this.opts.baseUrl,
|
|
370
389
|
getAuthToken: this.authTokenProvider ?? undefined,
|
|
390
|
+
recoverCredential: this.credentialRecovery ?? undefined,
|
|
371
391
|
}, { queries: [query] });
|
|
372
392
|
const rows = Array.isArray(result.results[0]) ? result.results[0] : [];
|
|
373
393
|
// Normalize: wire rows lack `__typename` when the server elides it.
|
|
@@ -523,8 +543,8 @@ async function scanIdb(database, modelName, clauses) {
|
|
|
523
543
|
// through to full-scan + filter.
|
|
524
544
|
if (clausesAreAllEquality(clauses)) {
|
|
525
545
|
const indexedKeys = Object.keys(eqClauses).filter((k) => k !== 'id' && typeof eqClauses[k] === 'string');
|
|
526
|
-
|
|
527
|
-
|
|
546
|
+
const idxKey = indexedKeys.length === 1 ? indexedKeys[0] : undefined;
|
|
547
|
+
if (idxKey !== undefined) {
|
|
528
548
|
try {
|
|
529
549
|
const rows = await store.getAllFromIndex(idxKey, eqClauses[idxKey]);
|
|
530
550
|
if (Array.isArray(rows)) {
|
|
@@ -59,8 +59,9 @@ export declare const probeResultSchema: z.ZodObject<{
|
|
|
59
59
|
export type ProbeResult = z.infer<typeof probeResultSchema>;
|
|
60
60
|
export interface NetworkProbeOptions {
|
|
61
61
|
/**
|
|
62
|
-
* Sync-server base URL (HTTP or WS scheme accepted). If omitted,
|
|
63
|
-
*
|
|
62
|
+
* Sync-server base URL (HTTP or WS scheme accepted). If omitted, the probe
|
|
63
|
+
* targets the canonical hosted endpoint ({@link ABLO_DEFAULT_BASE_URL}) —
|
|
64
|
+
* the same default `Ablo()` resolves.
|
|
64
65
|
*/
|
|
65
66
|
baseUrl?: string;
|
|
66
67
|
/**
|
|
@@ -28,6 +28,7 @@ import { z } from 'zod';
|
|
|
28
28
|
import { getContext } from '../context.js';
|
|
29
29
|
import { classifyRecovery } from '../errors.js';
|
|
30
30
|
import { withAuthHeaders } from '../auth/credentialSource.js';
|
|
31
|
+
import { ABLO_DEFAULT_BASE_URL } from '../client/hostedEndpoints.js';
|
|
31
32
|
/**
|
|
32
33
|
* The closed set of probe outcomes — one value carrying both reachability and
|
|
33
34
|
* credential disposition, so the {@link ConnectionManager} branches on a single
|
|
@@ -64,11 +65,11 @@ const PROBE_TIMEOUT_MS = 4000;
|
|
|
64
65
|
* normalisation in `BootstrapHelper` / `createSyncEngine`.
|
|
65
66
|
*/
|
|
66
67
|
function resolveProbeUrl(baseUrl) {
|
|
67
|
-
//
|
|
68
|
-
// to
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
// No explicit baseUrl → probe the canonical hosted endpoint, matching the
|
|
69
|
+
// `Ablo()` default. (This used to fall back to the REMOVED Go engine's
|
|
70
|
+
// `NEXT_PUBLIC_GO_SERVER_URL` and then `http://localhost:8080`, so a probe
|
|
71
|
+
// without a baseUrl reported a healthy production deployment as offline.)
|
|
72
|
+
const resolved = baseUrl ?? ABLO_DEFAULT_BASE_URL;
|
|
72
73
|
// Normalize ws → http so fetch() accepts the URL. Strip any trailing slash
|
|
73
74
|
// so we don't produce `//api/auth/check`.
|
|
74
75
|
const httpBase = resolved.replace(/^ws/, 'http').replace(/\/+$/, '');
|
|
@@ -98,7 +99,7 @@ export async function probeNetwork(input) {
|
|
|
98
99
|
return { outcome: 'unreachable', latencyMs: null };
|
|
99
100
|
}
|
|
100
101
|
const controller = new AbortController();
|
|
101
|
-
const timeout = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
102
|
+
const timeout = setTimeout(() => { controller.abort(); }, PROBE_TIMEOUT_MS);
|
|
102
103
|
const start = performance.now();
|
|
103
104
|
try {
|
|
104
105
|
const headers = withAuthHeaders(getAuthToken, { 'Cache-Control': 'no-cache' }, authToken);
|