@recordtimelabel/core 0.4.1 → 0.4.2
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/README.md +8 -2
- package/package.json +1 -1
- package/src/index.js +47 -5
package/README.md
CHANGED
|
@@ -17,10 +17,10 @@ During local development an app can consume a sibling checkout with:
|
|
|
17
17
|
"@recordtimelabel/core": "file:../recordtimelabel-core"
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
For release builds, consume a fixed npm package, git tag, or private registry version so builds do not depend on a sibling folder path. The durable identity and explicit entrypoint contract is prepared in package version `0.4.
|
|
20
|
+
For release builds, consume a fixed npm package, git tag, or private registry version so builds do not depend on a sibling folder path. The durable identity and explicit entrypoint contract is prepared in package version `0.4.2` (publish it before updating consumers):
|
|
21
21
|
|
|
22
22
|
```json
|
|
23
|
-
"@recordtimelabel/core": "0.4.
|
|
23
|
+
"@recordtimelabel/core": "0.4.2"
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
If this checkout's `package.json` is ahead of the published version, publish the new package before updating consumers to that version.
|
|
@@ -33,6 +33,7 @@ If this checkout's `package.json` is ahead of the published version, publish the
|
|
|
33
33
|
- `applyOperation(state, operation)`
|
|
34
34
|
- `applyRecordTimeLabelOperation(state, operation)`
|
|
35
35
|
- `createRecordTimeLabelSyncEngine({ storage, cloud, session, client, clock, logger })`
|
|
36
|
+
- `RECORD_TIMELABEL_DURABLE_ENGINE_CAPABILITIES`
|
|
36
37
|
- `normalizeRecordTimeLabelOperationResults(operations, results)`
|
|
37
38
|
- `normalizeRecordTimeLabelEnvelopeResponse(operations, response)`
|
|
38
39
|
- `createSyncEngine({ storageAdapter, cloudAdapter, clientId, clock, logger })`
|
|
@@ -102,6 +103,11 @@ operations missing them inherit the migrated workspace identity. An operation wh
|
|
|
102
103
|
explicit identity does not match the current workspace is moved to
|
|
103
104
|
`rejectedOperations` with a stable identity-mismatch reason before any cloud apply.
|
|
104
105
|
|
|
106
|
+
Pending operations also carry an internal `syncBatchId`. Operations dispatched in
|
|
107
|
+
one call share a batch boundary, while legacy pending operations are assigned one
|
|
108
|
+
stable boundary during migration. Adapters may use this field to preserve immutable
|
|
109
|
+
request envelopes across worker restarts; it must not be sent to the gateway.
|
|
110
|
+
|
|
105
111
|
The engine exposes `init()`, `dispatch(operations)`, `sync(reason)`, `getSnapshot()`,
|
|
106
112
|
`subscribe(listener)`, and `destroy()`. `getSnapshot().state` is derived by replaying pending
|
|
107
113
|
operations over `remoteBaseline.state`; rejected operations are kept in diagnostics and are not
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -17,8 +17,13 @@ const REQUIRED_FOLDERS = [
|
|
|
17
17
|
{ id: DEFAULT_FOLDER_ID, name: 'Uncategorized' }
|
|
18
18
|
];
|
|
19
19
|
|
|
20
|
-
export const RECORD_TIMELABEL_CORE_VERSION = '0.4.
|
|
20
|
+
export const RECORD_TIMELABEL_CORE_VERSION = '0.4.2';
|
|
21
21
|
export const RTL_SYNC_PROTOCOL_VERSION = 2;
|
|
22
|
+
export const RECORD_TIMELABEL_DURABLE_ENGINE_CAPABILITIES = Object.freeze([
|
|
23
|
+
'fifo-retry-fence',
|
|
24
|
+
'baseline-refresh-after-rejection',
|
|
25
|
+
'sync-batch-boundary'
|
|
26
|
+
]);
|
|
22
27
|
export const RTL_MAX_OPERATIONS_PER_REQUEST = 20;
|
|
23
28
|
export const RTL_MAX_REQUEST_BYTES = 256 * 1024;
|
|
24
29
|
export const RTL_MAX_TARGET_WRITES = 100;
|
|
@@ -2729,6 +2734,16 @@ const rtlNormalizePendingOperation = (operation, options = {}) => {
|
|
|
2729
2734
|
return normalized;
|
|
2730
2735
|
};
|
|
2731
2736
|
|
|
2737
|
+
const rtlStableSyncBatchId = (operations = []) => {
|
|
2738
|
+
const source = toArray(operations).map((operation) => normalizeId(operation?.id)).join('|');
|
|
2739
|
+
let hash = 2166136261;
|
|
2740
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
2741
|
+
hash ^= source.charCodeAt(index);
|
|
2742
|
+
hash = Math.imul(hash, 16777619);
|
|
2743
|
+
}
|
|
2744
|
+
return `durable-batch:${(hash >>> 0).toString(16).padStart(8, '0')}`;
|
|
2745
|
+
};
|
|
2746
|
+
|
|
2732
2747
|
const rtlNormalizeRemoteBaseline = (value = {}) => {
|
|
2733
2748
|
const baseline = rtlDurableIsObject(value) ? value : {};
|
|
2734
2749
|
return {
|
|
@@ -2772,6 +2787,10 @@ const rtlNormalizeDurableWorkspace = (input = {}, options = {}) => {
|
|
|
2772
2787
|
const pendingOperations = toArray(pendingSource)
|
|
2773
2788
|
.filter(Boolean)
|
|
2774
2789
|
.map((operation) => rtlNormalizePendingOperation(operation, options));
|
|
2790
|
+
const legacyBatchId = rtlStableSyncBatchId(pendingOperations);
|
|
2791
|
+
pendingOperations.forEach((operation) => {
|
|
2792
|
+
if (!operation.syncBatchId) operation.syncBatchId = legacyBatchId;
|
|
2793
|
+
});
|
|
2775
2794
|
const legacyExpandedGroups = Array.isArray(source.syncMeta?.legacyExpandedGroups)
|
|
2776
2795
|
? normalizeOrder(source.syncMeta.legacyExpandedGroups)
|
|
2777
2796
|
: Array.isArray(source.state?.expandedGroups)
|
|
@@ -3374,6 +3393,10 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
3374
3393
|
ownerUid: captured?.uid ?? candidate.ownerUid,
|
|
3375
3394
|
workspaceEpoch: captured?.workspaceEpoch ?? candidate.workspaceEpoch
|
|
3376
3395
|
}));
|
|
3396
|
+
const syncBatchId = rtlStableSyncBatchId(normalized);
|
|
3397
|
+
normalized.forEach((operation) => {
|
|
3398
|
+
if (!operation.syncBatchId) operation.syncBatchId = syncBatchId;
|
|
3399
|
+
});
|
|
3377
3400
|
const identityChecked = rtlQuarantineOperations(candidate, normalized, now());
|
|
3378
3401
|
const viewOperations = identityChecked.accepted.filter((operation) => (
|
|
3379
3402
|
operation.type === OPERATION_TYPES.EXPANDED_GROUPS_UPDATE
|
|
@@ -3446,9 +3469,15 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
3446
3469
|
workspace = candidate;
|
|
3447
3470
|
notify({type: 'operations_rejected', rejectedCount: identityRejectedCount});
|
|
3448
3471
|
}
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3472
|
+
// Pending operations are a FIFO log. A retry deadline on an earlier entry
|
|
3473
|
+
// is a head-of-line barrier; dependent operations behind it must not
|
|
3474
|
+
// overtake it merely because they do not have their own deadline yet.
|
|
3475
|
+
const ready = [];
|
|
3476
|
+
for (const operation of workspace.pendingOperations) {
|
|
3477
|
+
if (Number.isFinite(Number(operation.nextRetryAt)) &&
|
|
3478
|
+
Number(operation.nextRetryAt) > timestamp) break;
|
|
3479
|
+
ready.push(operation);
|
|
3480
|
+
}
|
|
3452
3481
|
if (ready.length === 0) {
|
|
3453
3482
|
return {
|
|
3454
3483
|
success: true,
|
|
@@ -3590,6 +3619,18 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
3590
3619
|
lastSyncReason: reason ?? null,
|
|
3591
3620
|
lastSyncError: null
|
|
3592
3621
|
};
|
|
3622
|
+
const rejectedCount = parsed.results.filter((result) => result.status === 'rejected').length;
|
|
3623
|
+
if (rejectedCount > 0 && typeof cloud?.bootstrap === 'function') {
|
|
3624
|
+
let freshBaseline;
|
|
3625
|
+
try {
|
|
3626
|
+
freshBaseline = await cloud.bootstrap(context);
|
|
3627
|
+
} catch (error) {
|
|
3628
|
+
if (!(await isCurrent(captured))) return {success: false, reason: 'stale_session'};
|
|
3629
|
+
throw error;
|
|
3630
|
+
}
|
|
3631
|
+
if (!(await isCurrent(captured))) return {success: false, reason: 'stale_session'};
|
|
3632
|
+
applyRemoteBaseline(candidate, freshBaseline);
|
|
3633
|
+
}
|
|
3593
3634
|
if (!(await persist(candidate, captured))) return {success: false, reason: 'stale_session'};
|
|
3594
3635
|
workspace = candidate;
|
|
3595
3636
|
notify({type: 'synced', operations: clone(parsed.results)});
|
|
@@ -3600,7 +3641,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
3600
3641
|
success: true,
|
|
3601
3642
|
appliedCount: appliedOperations.length,
|
|
3602
3643
|
syncedCount: appliedOperations.length + parsed.results.filter((result) => result.status === 'noop').length,
|
|
3603
|
-
rejectedCount
|
|
3644
|
+
rejectedCount,
|
|
3604
3645
|
retryCount: parsed.results.filter((result) => result.status === 'retryable').length,
|
|
3605
3646
|
retryAt: Number.isFinite(retryAt) ? retryAt : null,
|
|
3606
3647
|
pendingCount: workspace.pendingOperations.length,
|
|
@@ -4634,6 +4675,7 @@ export const buildOperationsFromSnapshotDiff = ({
|
|
|
4634
4675
|
|
|
4635
4676
|
export default {
|
|
4636
4677
|
RECORD_TIMELABEL_CORE_VERSION,
|
|
4678
|
+
RECORD_TIMELABEL_DURABLE_ENGINE_CAPABILITIES,
|
|
4637
4679
|
RECORD_TIMELABEL_OPERATION_RESULT_STATUSES,
|
|
4638
4680
|
RTL_SYNC_PROTOCOL_VERSION,
|
|
4639
4681
|
RTL_MAX_OPERATIONS_PER_REQUEST,
|