@recordtimelabel/core 0.6.6 → 0.6.8
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 +4 -2
- package/package.json +1 -1
- package/src/index.js +60 -23
- package/src/protocol.js +34 -0
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 release target is `0.6.
|
|
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 release target is `0.6.8`; verify that its registry tarball and lockfile integrity are available before updating consumers:
|
|
21
21
|
|
|
22
22
|
```json
|
|
23
|
-
"@recordtimelabel/core": "0.6.
|
|
23
|
+
"@recordtimelabel/core": "0.6.8"
|
|
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.
|
|
@@ -38,7 +38,9 @@ If this checkout's `package.json` is ahead of the published version, publish the
|
|
|
38
38
|
- `RECORD_TIMELABEL_DURABLE_ENGINE_CAPABILITIES`
|
|
39
39
|
- `RECORD_TIMELABEL_CAPABILITY_CLOUD_FAILURE_STATE`
|
|
40
40
|
- `RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES`
|
|
41
|
+
- `RECORD_TIMELABEL_CLOUD_RECOVERY_ACTIONS`
|
|
41
42
|
- `normalizeRecordTimeLabelCloudFailure(input)`
|
|
43
|
+
- `classifyRecordTimeLabelCloudRecovery(input)`
|
|
42
44
|
- `toRecordTimeLabelCloudFailureError(input)`
|
|
43
45
|
- `RECORD_TIMELABEL_PROTOCOL_CAPABILITIES`
|
|
44
46
|
- `toRecordTimeLabelWireOperation(operation)`
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -8,12 +8,14 @@ import {
|
|
|
8
8
|
RECORD_TIMELABEL_CAPABILITY_STRICT_READINESS,
|
|
9
9
|
RECORD_TIMELABEL_CLOUD_FAILURE_SCHEMA_VERSION,
|
|
10
10
|
RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES,
|
|
11
|
+
RECORD_TIMELABEL_CLOUD_RECOVERY_ACTIONS,
|
|
11
12
|
RECORD_TIMELABEL_OPERATION_RESULT_STATUSES,
|
|
12
13
|
RECORD_TIMELABEL_PROTOCOL_CAPABILITIES,
|
|
13
14
|
buildRecordTimeLabelRequestId,
|
|
14
15
|
createRecordTimeLabelTransportFailureResults,
|
|
15
16
|
isRecordTimeLabelOperationConflictFailure,
|
|
16
17
|
normalizeRecordTimeLabelCloudFailure,
|
|
18
|
+
classifyRecordTimeLabelCloudRecovery,
|
|
17
19
|
normalizeRecordTimeLabelEnvelopeResponse,
|
|
18
20
|
normalizeRecordTimeLabelOperationResults,
|
|
19
21
|
normalizeRecordTimeLabelImmutableId,
|
|
@@ -32,12 +34,14 @@ export {
|
|
|
32
34
|
RECORD_TIMELABEL_CAPABILITY_STRICT_READINESS,
|
|
33
35
|
RECORD_TIMELABEL_CLOUD_FAILURE_SCHEMA_VERSION,
|
|
34
36
|
RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES,
|
|
37
|
+
RECORD_TIMELABEL_CLOUD_RECOVERY_ACTIONS,
|
|
35
38
|
RECORD_TIMELABEL_OPERATION_RESULT_STATUSES,
|
|
36
39
|
RECORD_TIMELABEL_PROTOCOL_CAPABILITIES,
|
|
37
40
|
buildRecordTimeLabelRequestId,
|
|
38
41
|
createRecordTimeLabelTransportFailureResults,
|
|
39
42
|
isRecordTimeLabelOperationConflictFailure,
|
|
40
43
|
normalizeRecordTimeLabelCloudFailure,
|
|
44
|
+
classifyRecordTimeLabelCloudRecovery,
|
|
41
45
|
normalizeRecordTimeLabelEnvelopeResponse,
|
|
42
46
|
normalizeRecordTimeLabelOperationResults,
|
|
43
47
|
normalizeRecordTimeLabelImmutableId,
|
|
@@ -141,7 +145,7 @@ export {
|
|
|
141
145
|
selectBestMatchingTwitchVod
|
|
142
146
|
};
|
|
143
147
|
|
|
144
|
-
export const RECORD_TIMELABEL_CORE_VERSION = '0.6.
|
|
148
|
+
export const RECORD_TIMELABEL_CORE_VERSION = '0.6.8';
|
|
145
149
|
export const RTL_SYNC_PROTOCOL_VERSION = 2;
|
|
146
150
|
export const RECORD_TIMELABEL_DURABLE_ENGINE_CAPABILITIES = Object.freeze([
|
|
147
151
|
'fifo-retry-fence',
|
|
@@ -4044,6 +4048,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4044
4048
|
})
|
|
4045
4049
|
};
|
|
4046
4050
|
let bootstrapAttemptSequence = 0;
|
|
4051
|
+
const activeBootstrapAttempts = new Map();
|
|
4047
4052
|
let authTransitionLatch = null;
|
|
4048
4053
|
const listeners = new Set();
|
|
4049
4054
|
let queue = Promise.resolve();
|
|
@@ -4164,7 +4169,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4164
4169
|
return {changed, stale: false};
|
|
4165
4170
|
};
|
|
4166
4171
|
|
|
4167
|
-
const
|
|
4172
|
+
const createBootstrapAttemptOptions = (mode) => ({
|
|
4168
4173
|
mode,
|
|
4169
4174
|
attemptId: `bootstrap:${mode}:${now()}:${++bootstrapAttemptSequence}:${
|
|
4170
4175
|
globalThis.crypto?.randomUUID?.() || Math.random().toString(36).slice(2, 14)
|
|
@@ -4172,6 +4177,27 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4172
4177
|
requireFresh: true
|
|
4173
4178
|
});
|
|
4174
4179
|
|
|
4180
|
+
const bootstrapAttemptIdentity = (context = {}) => JSON.stringify([
|
|
4181
|
+
context.uid ?? null,
|
|
4182
|
+
context.sessionToken ?? null,
|
|
4183
|
+
Number(context.workspaceEpoch || 0)
|
|
4184
|
+
]);
|
|
4185
|
+
|
|
4186
|
+
const bootstrapAttemptOptions = (mode, context) => {
|
|
4187
|
+
const identity = bootstrapAttemptIdentity(context);
|
|
4188
|
+
const active = activeBootstrapAttempts.get(mode);
|
|
4189
|
+
if (active?.identity === identity) return active.options;
|
|
4190
|
+
const options = createBootstrapAttemptOptions(mode);
|
|
4191
|
+
activeBootstrapAttempts.set(mode, {identity, options});
|
|
4192
|
+
return options;
|
|
4193
|
+
};
|
|
4194
|
+
|
|
4195
|
+
const clearBootstrapAttempt = (mode, options) => {
|
|
4196
|
+
if (activeBootstrapAttempts.get(mode)?.options === options) {
|
|
4197
|
+
activeBootstrapAttempts.delete(mode);
|
|
4198
|
+
}
|
|
4199
|
+
};
|
|
4200
|
+
|
|
4175
4201
|
const normalizeBootstrapResponse = (response) => {
|
|
4176
4202
|
if (!rtlEnvelopeSuccess(response)) {
|
|
4177
4203
|
const source = response && typeof response === 'object' ? response : {
|
|
@@ -4196,6 +4222,24 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4196
4222
|
return {state, revision, changeCursor: remote.changeCursor ?? null};
|
|
4197
4223
|
};
|
|
4198
4224
|
|
|
4225
|
+
// A transient failure belongs to the same logical page walk. Keep its
|
|
4226
|
+
// attempt ID so the platform bootstrap owner can resume the signed cursor;
|
|
4227
|
+
// successful and non-transient outcomes retire the attempt.
|
|
4228
|
+
const runBootstrapAttempt = async (context, mode) => {
|
|
4229
|
+
const options = bootstrapAttemptOptions(mode, context);
|
|
4230
|
+
try {
|
|
4231
|
+
const baseline = normalizeBootstrapResponse(await cloud.bootstrap(context, options));
|
|
4232
|
+
clearBootstrapAttempt(mode, options);
|
|
4233
|
+
return baseline;
|
|
4234
|
+
} catch (error) {
|
|
4235
|
+
const failure = normalizeRecordTimeLabelCloudFailure(error);
|
|
4236
|
+
if (failure.class !== RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES.TRANSIENT) {
|
|
4237
|
+
clearBootstrapAttempt(mode, options);
|
|
4238
|
+
}
|
|
4239
|
+
throw error;
|
|
4240
|
+
}
|
|
4241
|
+
};
|
|
4242
|
+
|
|
4199
4243
|
const clearAuthTransitionLatchIfSessionChanged = (captured) => {
|
|
4200
4244
|
if (!authTransitionLatch) return;
|
|
4201
4245
|
if (!rtlSameSessionIdentity(authTransitionLatch, captured)) {
|
|
@@ -4279,7 +4323,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4279
4323
|
const caught = await cloud.catchUp(context, {
|
|
4280
4324
|
fromRevision: workspace.remoteBaseline.revision,
|
|
4281
4325
|
targetRevision,
|
|
4282
|
-
...
|
|
4326
|
+
...createBootstrapAttemptOptions(`${mode}-catchup`)
|
|
4283
4327
|
});
|
|
4284
4328
|
if (rtlEnvelopeSuccess(caught)) {
|
|
4285
4329
|
return requireBootstrapRevision(normalizeBootstrapResponse(caught), targetRevision);
|
|
@@ -4304,10 +4348,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4304
4348
|
throwIfAuthTransitionLatched(captured);
|
|
4305
4349
|
try {
|
|
4306
4350
|
return requireBootstrapRevision(
|
|
4307
|
-
|
|
4308
|
-
context,
|
|
4309
|
-
bootstrapAttemptOptions(mode)
|
|
4310
|
-
)),
|
|
4351
|
+
await runBootstrapAttempt(context, mode),
|
|
4311
4352
|
targetRevision
|
|
4312
4353
|
);
|
|
4313
4354
|
} catch (error) {
|
|
@@ -4581,10 +4622,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4581
4622
|
}
|
|
4582
4623
|
let baseline;
|
|
4583
4624
|
try {
|
|
4584
|
-
baseline =
|
|
4585
|
-
context,
|
|
4586
|
-
bootstrapAttemptOptions('apply-failure-rebase')
|
|
4587
|
-
));
|
|
4625
|
+
baseline = await runBootstrapAttempt(context, 'apply-failure-rebase');
|
|
4588
4626
|
} catch (bootstrapError) {
|
|
4589
4627
|
if (!(await isCurrent(captured))) return {success: false, reason: 'stale_session'};
|
|
4590
4628
|
rethrowClassifiedCloudFailure(captured, bootstrapError);
|
|
@@ -4751,10 +4789,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4751
4789
|
if (typeof cloud?.bootstrap === 'function') {
|
|
4752
4790
|
try {
|
|
4753
4791
|
freshBaseline = requireBootstrapRevision(
|
|
4754
|
-
|
|
4755
|
-
context,
|
|
4756
|
-
bootstrapAttemptOptions('rejection-rebase')
|
|
4757
|
-
)),
|
|
4792
|
+
await runBootstrapAttempt(context, 'rejection-rebase'),
|
|
4758
4793
|
requiredRevision
|
|
4759
4794
|
);
|
|
4760
4795
|
} catch (bootstrapError) {
|
|
@@ -5249,10 +5284,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
5249
5284
|
});
|
|
5250
5285
|
let bootstrap;
|
|
5251
5286
|
try {
|
|
5252
|
-
bootstrap =
|
|
5253
|
-
context,
|
|
5254
|
-
bootstrapAttemptOptions('initial-hydration')
|
|
5255
|
-
));
|
|
5287
|
+
bootstrap = await runBootstrapAttempt(context, 'initial-hydration');
|
|
5256
5288
|
} catch (error) {
|
|
5257
5289
|
if (!(await isCurrent(captured))) return getSnapshot();
|
|
5258
5290
|
if (persistedHydrationRequired) {
|
|
@@ -5779,10 +5811,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
5779
5811
|
let freshBaseline;
|
|
5780
5812
|
try {
|
|
5781
5813
|
freshBaseline = requireBootstrapRevision(
|
|
5782
|
-
|
|
5783
|
-
context,
|
|
5784
|
-
bootstrapAttemptOptions('rejection-rebase')
|
|
5785
|
-
)),
|
|
5814
|
+
await runBootstrapAttempt(context, 'rejection-rebase'),
|
|
5786
5815
|
responseRevision
|
|
5787
5816
|
);
|
|
5788
5817
|
} catch (error) {
|
|
@@ -5930,6 +5959,12 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
5930
5959
|
const captured = capture();
|
|
5931
5960
|
clearAuthTransitionLatchIfSessionChanged(captured);
|
|
5932
5961
|
if (initialized && !hydrationRequired && workspaceMatchesSession(captured)) {
|
|
5962
|
+
// A transient listener failure stops only the provider subscription.
|
|
5963
|
+
// Re-entering init for the same owner must reattach that subscription
|
|
5964
|
+
// without discarding the committed workspace or bootstrapping again.
|
|
5965
|
+
if (captured?.uid && !subscriptionContext && typeof cloud?.subscribe === 'function') {
|
|
5966
|
+
startCloudSubscription(captured, workspace);
|
|
5967
|
+
}
|
|
5933
5968
|
return getSnapshot();
|
|
5934
5969
|
}
|
|
5935
5970
|
if (initialized && !hydrationRequired && !workspaceMatchesSession(captured)) {
|
|
@@ -7327,8 +7362,10 @@ export default {
|
|
|
7327
7362
|
RECORD_TIMELABEL_CAPABILITY_STRICT_READINESS,
|
|
7328
7363
|
RECORD_TIMELABEL_CLOUD_FAILURE_SCHEMA_VERSION,
|
|
7329
7364
|
RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES,
|
|
7365
|
+
RECORD_TIMELABEL_CLOUD_RECOVERY_ACTIONS,
|
|
7330
7366
|
isRecordTimeLabelOperationConflictFailure,
|
|
7331
7367
|
normalizeRecordTimeLabelCloudFailure,
|
|
7368
|
+
classifyRecordTimeLabelCloudRecovery,
|
|
7332
7369
|
toRecordTimeLabelCloudFailureError,
|
|
7333
7370
|
RECORD_TIMELABEL_OPERATION_RESULT_STATUSES,
|
|
7334
7371
|
RECORD_TIMELABEL_PROTOCOL_CAPABILITIES,
|
package/src/protocol.js
CHANGED
|
@@ -73,6 +73,16 @@ export const RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES = Object.freeze({
|
|
|
73
73
|
STALE_SESSION: 'stale-session'
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
// Hosts must map class to these actions. They may choose the platform I/O
|
|
77
|
+
// (alarm, timer, Web Lock, Main process), but they must not invent a fourth
|
|
78
|
+
// strategy such as "rebuild the owner on 429".
|
|
79
|
+
export const RECORD_TIMELABEL_CLOUD_RECOVERY_ACTIONS = Object.freeze({
|
|
80
|
+
IGNORE: 'ignore',
|
|
81
|
+
WAIT: 'wait',
|
|
82
|
+
BOOTSTRAP: 'bootstrap',
|
|
83
|
+
LOCK: 'lock'
|
|
84
|
+
});
|
|
85
|
+
|
|
76
86
|
const CLOUD_FAILURE_CLASS_VALUES = new Set(Object.values(RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES));
|
|
77
87
|
|
|
78
88
|
// Keep operation-conflict aliases in one wire-compatibility table. The
|
|
@@ -580,6 +590,28 @@ export const toRecordTimeLabelCloudFailureError = (input = {}) => {
|
|
|
580
590
|
return error;
|
|
581
591
|
};
|
|
582
592
|
|
|
593
|
+
/**
|
|
594
|
+
* Shared recovery mapping for cloud-failure-state-v1. Core owns the class →
|
|
595
|
+
* action table so Extension and Flowmoor cannot fork "retry" into bootstrap.
|
|
596
|
+
*
|
|
597
|
+
* `wait` keeps the workspace, outbox, and cursor/attempt and honors
|
|
598
|
+
* `retryAfterMs`. It is not permission to rebuild an owner or start a page-1
|
|
599
|
+
* walk. `bootstrap` is the only action that may start a fresh hydration.
|
|
600
|
+
*/
|
|
601
|
+
export const classifyRecordTimeLabelCloudRecovery = (input = {}) => {
|
|
602
|
+
const failure = normalizeRecordTimeLabelCloudFailure(input);
|
|
603
|
+
if (failure.class === RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES.STALE_SESSION) {
|
|
604
|
+
return {action: RECORD_TIMELABEL_CLOUD_RECOVERY_ACTIONS.IGNORE, failure};
|
|
605
|
+
}
|
|
606
|
+
if (failure.class === RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES.TRANSIENT) {
|
|
607
|
+
return {action: RECORD_TIMELABEL_CLOUD_RECOVERY_ACTIONS.WAIT, failure};
|
|
608
|
+
}
|
|
609
|
+
if (failure.class === RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES.BOOTSTRAP_REQUIRED) {
|
|
610
|
+
return {action: RECORD_TIMELABEL_CLOUD_RECOVERY_ACTIONS.BOOTSTRAP, failure};
|
|
611
|
+
}
|
|
612
|
+
return {action: RECORD_TIMELABEL_CLOUD_RECOVERY_ACTIONS.LOCK, failure};
|
|
613
|
+
};
|
|
614
|
+
|
|
583
615
|
const ALLOWED_OPERATION_RESULT_STATUSES = new Set(
|
|
584
616
|
Object.values(RECORD_TIMELABEL_OPERATION_RESULT_STATUSES)
|
|
585
617
|
);
|
|
@@ -833,6 +865,7 @@ export default {
|
|
|
833
865
|
RECORD_TIMELABEL_CAPABILITY_LIFECYCLE_GENERATION_FENCE_V1,
|
|
834
866
|
RECORD_TIMELABEL_CLOUD_FAILURE_SCHEMA_VERSION,
|
|
835
867
|
RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES,
|
|
868
|
+
RECORD_TIMELABEL_CLOUD_RECOVERY_ACTIONS,
|
|
836
869
|
RECORD_TIMELABEL_PROTOCOL_CAPABILITIES,
|
|
837
870
|
toRecordTimeLabelWireOperation,
|
|
838
871
|
buildRecordTimeLabelRequestId,
|
|
@@ -840,6 +873,7 @@ export default {
|
|
|
840
873
|
normalizeRecordTimeLabelOperationResults,
|
|
841
874
|
normalizeRecordTimeLabelEnvelopeResponse,
|
|
842
875
|
normalizeRecordTimeLabelCloudFailure,
|
|
876
|
+
classifyRecordTimeLabelCloudRecovery,
|
|
843
877
|
toRecordTimeLabelCloudFailureError,
|
|
844
878
|
isRecordTimeLabelOperationConflictFailure,
|
|
845
879
|
normalizeRecordTimeLabelImmutableId,
|