@recordtimelabel/core 0.4.4 → 0.4.5
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/package.json +1 -1
- package/src/index.js +49 -17
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -31,7 +31,7 @@ const REQUIRED_FOLDERS = [
|
|
|
31
31
|
{ id: DEFAULT_FOLDER_ID, name: 'Uncategorized' }
|
|
32
32
|
];
|
|
33
33
|
|
|
34
|
-
export const RECORD_TIMELABEL_CORE_VERSION = '0.4.
|
|
34
|
+
export const RECORD_TIMELABEL_CORE_VERSION = '0.4.5';
|
|
35
35
|
export const RTL_SYNC_PROTOCOL_VERSION = 2;
|
|
36
36
|
export const RECORD_TIMELABEL_DURABLE_ENGINE_CAPABILITIES = Object.freeze([
|
|
37
37
|
'fifo-retry-fence',
|
|
@@ -3134,11 +3134,15 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
3134
3134
|
session,
|
|
3135
3135
|
client = 'recordtimelabel-client',
|
|
3136
3136
|
clock = () => Date.now(),
|
|
3137
|
-
logger = console
|
|
3137
|
+
logger = console,
|
|
3138
|
+
initialBaseline = 'require-fresh'
|
|
3138
3139
|
} = {}) => {
|
|
3139
3140
|
if (!storage?.load || !storage?.save) {
|
|
3140
3141
|
throw new Error('createRecordTimeLabelSyncEngine requires storage.load() and storage.save()');
|
|
3141
3142
|
}
|
|
3143
|
+
if (initialBaseline !== 'require-fresh' && initialBaseline !== 'reuse-committed') {
|
|
3144
|
+
throw new Error('recordtimelabel_invalid_initial_baseline_policy');
|
|
3145
|
+
}
|
|
3142
3146
|
|
|
3143
3147
|
let workspace = rtlEmptyDurableWorkspace();
|
|
3144
3148
|
let initialized = false;
|
|
@@ -3277,6 +3281,37 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
3277
3281
|
return baseline;
|
|
3278
3282
|
};
|
|
3279
3283
|
|
|
3284
|
+
// Optional bounded catch-up port. `cloud.catchUp(context, {fromRevision,
|
|
3285
|
+
// targetRevision, attemptId, mode})` resolves a FULL baseline object
|
|
3286
|
+
// (`{success: true, state, revision, changeCursor?}`, the same shape
|
|
3287
|
+
// normalizeBootstrapResponse accepts) or `{success: false,
|
|
3288
|
+
// bootstrapRequired: true}`; it may throw on transport errors. Gap
|
|
3289
|
+
// recovery prefers it over `cloud.bootstrap` and falls back to a fresh
|
|
3290
|
+
// bootstrap walk whenever it fails or is unavailable.
|
|
3291
|
+
const recoverBaseline = async (context, targetRevision, mode) => {
|
|
3292
|
+
if (typeof cloud?.catchUp === 'function') {
|
|
3293
|
+
try {
|
|
3294
|
+
const caught = await cloud.catchUp(context, {
|
|
3295
|
+
fromRevision: workspace.remoteBaseline.revision,
|
|
3296
|
+
targetRevision,
|
|
3297
|
+
...bootstrapAttemptOptions(`${mode}-catchup`)
|
|
3298
|
+
});
|
|
3299
|
+
if (rtlEnvelopeSuccess(caught)) {
|
|
3300
|
+
return requireBootstrapRevision(normalizeBootstrapResponse(caught), targetRevision);
|
|
3301
|
+
}
|
|
3302
|
+
} catch (error) {
|
|
3303
|
+
logger?.warn?.('[RecordTimeLabelCore] catch-up failed, falling back to bootstrap', error);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
return requireBootstrapRevision(
|
|
3307
|
+
normalizeBootstrapResponse(await cloud.bootstrap(
|
|
3308
|
+
context,
|
|
3309
|
+
bootstrapAttemptOptions(mode)
|
|
3310
|
+
)),
|
|
3311
|
+
targetRevision
|
|
3312
|
+
);
|
|
3313
|
+
};
|
|
3314
|
+
|
|
3280
3315
|
const normalizeLoadedWorkspace = (loaded, captured) => {
|
|
3281
3316
|
const source = loaded && typeof loaded === 'object' ? loaded : {};
|
|
3282
3317
|
const hasDurableShape = source.remoteBaseline || source.pendingOperations || source.schemaVersion === RTL_DURABLE_SCHEMA_VERSION;
|
|
@@ -3460,13 +3495,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
3460
3495
|
return {success: false, reason: 'revision_gap', bootstrapRequired: true};
|
|
3461
3496
|
}
|
|
3462
3497
|
const context = rtlSessionContext(captured, candidate, client);
|
|
3463
|
-
baselineValue =
|
|
3464
|
-
normalizeBootstrapResponse(await cloud.bootstrap(
|
|
3465
|
-
context,
|
|
3466
|
-
bootstrapAttemptOptions('gap-recovery')
|
|
3467
|
-
)),
|
|
3468
|
-
remoteRevision
|
|
3469
|
-
);
|
|
3498
|
+
baselineValue = await recoverBaseline(context, remoteRevision, 'gap-recovery');
|
|
3470
3499
|
if (!(await isCurrent(captured))) return {stale: true, reason: 'stale_session'};
|
|
3471
3500
|
}
|
|
3472
3501
|
const applied = applyRemoteBaseline(candidate, baselineValue);
|
|
@@ -3505,6 +3534,12 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
3505
3534
|
const captured = capture();
|
|
3506
3535
|
const loaded = await storage.load();
|
|
3507
3536
|
if (!(await isCurrent(captured))) return getSnapshot();
|
|
3537
|
+
// The reuse predicate must read the RAW loaded owner before
|
|
3538
|
+
// normalizeLoadedWorkspace adopts the current session identity onto the
|
|
3539
|
+
// workspace; epochs are intentionally not compared (consumers may advance
|
|
3540
|
+
// the persisted epoch on every launch).
|
|
3541
|
+
const loadedOwnerUid = rtlNormalizeUid(loaded?.ownerUid);
|
|
3542
|
+
const committedRevision = Number(loaded?.remoteBaseline?.revision || 0);
|
|
3508
3543
|
let candidate = normalizeLoadedWorkspace(loaded, captured);
|
|
3509
3544
|
const identityChecked = rtlQuarantineOperations(
|
|
3510
3545
|
candidate,
|
|
@@ -3515,7 +3550,10 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
3515
3550
|
candidate.rejectedOperations = identityChecked.rejected;
|
|
3516
3551
|
const context = rtlSessionContext(captured, candidate, client);
|
|
3517
3552
|
|
|
3518
|
-
|
|
3553
|
+
const canReuseCommitted = initialBaseline === 'reuse-committed' &&
|
|
3554
|
+
captured?.uid && loadedOwnerUid === rtlNormalizeUid(captured.uid) &&
|
|
3555
|
+
committedRevision > 0;
|
|
3556
|
+
if (captured?.uid && typeof cloud?.bootstrap === 'function' && !canReuseCommitted) {
|
|
3519
3557
|
let bootstrap;
|
|
3520
3558
|
try {
|
|
3521
3559
|
bootstrap = normalizeBootstrapResponse(await cloud.bootstrap(
|
|
@@ -3836,13 +3874,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
3836
3874
|
responseRevision > candidate.remoteBaseline.revision + 1 &&
|
|
3837
3875
|
typeof cloud?.bootstrap === 'function'
|
|
3838
3876
|
) {
|
|
3839
|
-
const freshBaseline =
|
|
3840
|
-
normalizeBootstrapResponse(await cloud.bootstrap(
|
|
3841
|
-
context,
|
|
3842
|
-
bootstrapAttemptOptions('gap-recovery')
|
|
3843
|
-
)),
|
|
3844
|
-
responseRevision
|
|
3845
|
-
);
|
|
3877
|
+
const freshBaseline = await recoverBaseline(context, responseRevision, 'gap-recovery');
|
|
3846
3878
|
if (!(await isCurrent(captured))) return {success: false, reason: 'stale_session'};
|
|
3847
3879
|
applyRemoteBaseline(candidate, freshBaseline);
|
|
3848
3880
|
}
|