@recordtimelabel/core 0.6.9 → 0.6.11
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 +2 -2
- package/package.json +1 -1
- package/src/domain.js +1 -0
- package/src/index.js +121 -12
- package/src/protocol.js +8 -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.11`; 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.11"
|
|
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.
|
package/package.json
CHANGED
package/src/domain.js
CHANGED
package/src/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
RECORD_TIMELABEL_CAPABILITY_STRICT_OPERATION_RESULTS,
|
|
8
8
|
RECORD_TIMELABEL_CAPABILITY_STRICT_READINESS,
|
|
9
9
|
RECORD_TIMELABEL_CLOUD_FAILURE_SCHEMA_VERSION,
|
|
10
|
+
RECORD_TIMELABEL_CLOUD_FAILURE_CODE_LOCAL_STORAGE_QUOTA_EXCEEDED,
|
|
10
11
|
RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES,
|
|
11
12
|
RECORD_TIMELABEL_CLOUD_RECOVERY_ACTIONS,
|
|
12
13
|
RECORD_TIMELABEL_OPERATION_RESULT_STATUSES,
|
|
@@ -33,6 +34,7 @@ export {
|
|
|
33
34
|
RECORD_TIMELABEL_CAPABILITY_STRICT_OPERATION_RESULTS,
|
|
34
35
|
RECORD_TIMELABEL_CAPABILITY_STRICT_READINESS,
|
|
35
36
|
RECORD_TIMELABEL_CLOUD_FAILURE_SCHEMA_VERSION,
|
|
37
|
+
RECORD_TIMELABEL_CLOUD_FAILURE_CODE_LOCAL_STORAGE_QUOTA_EXCEEDED,
|
|
36
38
|
RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES,
|
|
37
39
|
RECORD_TIMELABEL_CLOUD_RECOVERY_ACTIONS,
|
|
38
40
|
RECORD_TIMELABEL_OPERATION_RESULT_STATUSES,
|
|
@@ -145,13 +147,14 @@ export {
|
|
|
145
147
|
selectBestMatchingTwitchVod
|
|
146
148
|
};
|
|
147
149
|
|
|
148
|
-
export const RECORD_TIMELABEL_CORE_VERSION = '0.6.
|
|
150
|
+
export const RECORD_TIMELABEL_CORE_VERSION = '0.6.11';
|
|
149
151
|
export const RTL_SYNC_PROTOCOL_VERSION = 2;
|
|
150
152
|
export const RECORD_TIMELABEL_DURABLE_ENGINE_CAPABILITIES = Object.freeze([
|
|
151
153
|
'fifo-retry-fence',
|
|
152
154
|
'baseline-refresh-after-rejection',
|
|
153
155
|
'sync-batch-boundary',
|
|
154
156
|
'remote-subscription-readiness',
|
|
157
|
+
'twitch-vod-sibling-reconciliation-v1',
|
|
155
158
|
RECORD_TIMELABEL_CAPABILITY_OPERATION_CONFLICT_QUARANTINE,
|
|
156
159
|
RECORD_TIMELABEL_CAPABILITY_STRICT_OPERATION_RESULTS,
|
|
157
160
|
RECORD_TIMELABEL_CAPABILITY_LIFECYCLE_GENERATION_FENCE,
|
|
@@ -1484,6 +1487,27 @@ export const applyOperation = (state = {}, operation = {}) => {
|
|
|
1484
1487
|
|
|
1485
1488
|
export const applyRecordTimeLabelOperation = (state = {}, operation = {}) => applyOperation(state, operation);
|
|
1486
1489
|
|
|
1490
|
+
const collectRecordTimeLabelDomainRecords = (records = {}) => Object.entries(records || {})
|
|
1491
|
+
.flatMap(([folderId, folderRecords]) => (
|
|
1492
|
+
folderId === 'all' || !Array.isArray(folderRecords) ? [] : folderRecords
|
|
1493
|
+
));
|
|
1494
|
+
|
|
1495
|
+
export const buildRecordTimeLabelVodSiblingOperations = ({
|
|
1496
|
+
state = {},
|
|
1497
|
+
fallbackPlatform = 'unknown',
|
|
1498
|
+
unknownTitle
|
|
1499
|
+
} = {}) => {
|
|
1500
|
+
const normalized = normalizeState(state);
|
|
1501
|
+
const result = buildKnownTwitchVodRecordPatches({
|
|
1502
|
+
records: collectRecordTimeLabelDomainRecords(normalized.records),
|
|
1503
|
+
fallbackPlatform,
|
|
1504
|
+
unknownTitle
|
|
1505
|
+
});
|
|
1506
|
+
return result.patches.map(({recordId, patch}) => (
|
|
1507
|
+
buildRecordTimeLabelVodPatchOperation({recordId, patch})
|
|
1508
|
+
));
|
|
1509
|
+
};
|
|
1510
|
+
|
|
1487
1511
|
export const mergeLocalRemote = ({
|
|
1488
1512
|
localState = {},
|
|
1489
1513
|
remoteState = {},
|
|
@@ -3028,6 +3052,22 @@ export const createRecordTimeLabelController = ({
|
|
|
3028
3052
|
});
|
|
3029
3053
|
};
|
|
3030
3054
|
|
|
3055
|
+
const reconcileVodSiblings = () => {
|
|
3056
|
+
const pendingIds = new Set(pendingOps.map((operation) => operation?.id).filter(Boolean));
|
|
3057
|
+
const operations = buildRecordTimeLabelVodSiblingOperations({state})
|
|
3058
|
+
.filter((operation) => !pendingIds.has(operation.id))
|
|
3059
|
+
.map((operation) => ({
|
|
3060
|
+
...operation,
|
|
3061
|
+
clientId: operation.clientId || clientId,
|
|
3062
|
+
createdAt: operation.createdAt || clock()
|
|
3063
|
+
}));
|
|
3064
|
+
operations.forEach((operation) => {
|
|
3065
|
+
state = applyRecordTimeLabelOperation(state, operation);
|
|
3066
|
+
});
|
|
3067
|
+
if (operations.length > 0) pendingOps = [...pendingOps, ...operations];
|
|
3068
|
+
return operations;
|
|
3069
|
+
};
|
|
3070
|
+
|
|
3031
3071
|
const controller = {
|
|
3032
3072
|
async init() {
|
|
3033
3073
|
const loaded = await storageAdapter.load(storageKeys);
|
|
@@ -3035,6 +3075,8 @@ export const createRecordTimeLabelController = ({
|
|
|
3035
3075
|
pendingOps = toArray(loaded?.pendingOps).map((operation) => ({ ...operation }));
|
|
3036
3076
|
syncMeta = loaded?.syncMeta && typeof loaded.syncMeta === 'object' ? { ...loaded.syncMeta } : {};
|
|
3037
3077
|
|
|
3078
|
+
let shouldPersist = false;
|
|
3079
|
+
|
|
3038
3080
|
if (cloudAdapter?.load) {
|
|
3039
3081
|
const remoteState = await cloudAdapter.load();
|
|
3040
3082
|
const merged = mergeRemoteStateIntoLocal({
|
|
@@ -3047,8 +3089,20 @@ export const createRecordTimeLabelController = ({
|
|
|
3047
3089
|
now: clock()
|
|
3048
3090
|
});
|
|
3049
3091
|
state = merged.state;
|
|
3050
|
-
|
|
3092
|
+
shouldPersist = true;
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
const reconciledOperations = reconcileVodSiblings();
|
|
3096
|
+
if (reconciledOperations.length > 0) {
|
|
3097
|
+
const lastOperation = reconciledOperations.at(-1);
|
|
3098
|
+
syncMeta = {
|
|
3099
|
+
...syncMeta,
|
|
3100
|
+
lastLocalOperationAt: lastOperation.createdAt,
|
|
3101
|
+
lastLocalOperationType: lastOperation.type
|
|
3102
|
+
};
|
|
3103
|
+
shouldPersist = true;
|
|
3051
3104
|
}
|
|
3105
|
+
if (shouldPersist) await persist();
|
|
3052
3106
|
|
|
3053
3107
|
if (cloudAdapter?.subscribe) {
|
|
3054
3108
|
unsubscribeCloud = cloudAdapter.subscribe(async (remoteState) => {
|
|
@@ -3079,13 +3133,19 @@ export const createRecordTimeLabelController = ({
|
|
|
3079
3133
|
};
|
|
3080
3134
|
state = applyRecordTimeLabelOperation(state, nextOperation);
|
|
3081
3135
|
pendingOps = [...pendingOps, nextOperation];
|
|
3136
|
+
const reconciledOperations = reconcileVodSiblings();
|
|
3137
|
+
const lastOperation = reconciledOperations.at(-1) || nextOperation;
|
|
3082
3138
|
syncMeta = {
|
|
3083
3139
|
...syncMeta,
|
|
3084
|
-
lastLocalOperationAt:
|
|
3085
|
-
lastLocalOperationType:
|
|
3140
|
+
lastLocalOperationAt: lastOperation.createdAt,
|
|
3141
|
+
lastLocalOperationType: lastOperation.type
|
|
3086
3142
|
};
|
|
3087
3143
|
await persist();
|
|
3088
|
-
notify({
|
|
3144
|
+
notify({
|
|
3145
|
+
type: 'local_applied',
|
|
3146
|
+
operation: nextOperation,
|
|
3147
|
+
operations: [nextOperation, ...reconciledOperations]
|
|
3148
|
+
});
|
|
3089
3149
|
return getSnapshot();
|
|
3090
3150
|
},
|
|
3091
3151
|
|
|
@@ -3100,8 +3160,17 @@ export const createRecordTimeLabelController = ({
|
|
|
3100
3160
|
now: clock()
|
|
3101
3161
|
});
|
|
3102
3162
|
state = merged.state;
|
|
3163
|
+
const reconciledOperations = reconcileVodSiblings();
|
|
3164
|
+
if (reconciledOperations.length > 0) {
|
|
3165
|
+
const lastOperation = reconciledOperations.at(-1);
|
|
3166
|
+
syncMeta = {
|
|
3167
|
+
...syncMeta,
|
|
3168
|
+
lastLocalOperationAt: lastOperation.createdAt,
|
|
3169
|
+
lastLocalOperationType: lastOperation.type
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3103
3172
|
await persist();
|
|
3104
|
-
notify({ type: 'remote_merged' });
|
|
3173
|
+
notify({ type: 'remote_merged', operations: reconciledOperations });
|
|
3105
3174
|
return getSnapshot();
|
|
3106
3175
|
},
|
|
3107
3176
|
|
|
@@ -4169,6 +4238,32 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4169
4238
|
return {changed, stale: false};
|
|
4170
4239
|
};
|
|
4171
4240
|
|
|
4241
|
+
const appendVodSiblingReconciliation = (candidate, captured, {syncBatchId = null} = {}) => {
|
|
4242
|
+
const operations = buildRecordTimeLabelVodSiblingOperations({
|
|
4243
|
+
state: rtlDeriveDurableVisibleState(candidate)
|
|
4244
|
+
});
|
|
4245
|
+
if (operations.length === 0) return [];
|
|
4246
|
+
|
|
4247
|
+
const normalized = operations.map((operation) => rtlNormalizePendingOperation(operation, {
|
|
4248
|
+
client,
|
|
4249
|
+
clientId: typeof client === 'string' ? client : client?.id,
|
|
4250
|
+
now,
|
|
4251
|
+
ownerUid: captured?.uid ?? candidate.ownerUid,
|
|
4252
|
+
workspaceEpoch: captured?.workspaceEpoch ?? candidate.workspaceEpoch
|
|
4253
|
+
}));
|
|
4254
|
+
const reconciliationBatchId = syncBatchId || rtlStableSyncBatchId(normalized);
|
|
4255
|
+
normalized.forEach((operation) => {
|
|
4256
|
+
if (!operation.syncBatchId) operation.syncBatchId = reconciliationBatchId;
|
|
4257
|
+
});
|
|
4258
|
+
const identityChecked = rtlQuarantineOperations(candidate, normalized, now());
|
|
4259
|
+
candidate.pendingOperations = [
|
|
4260
|
+
...candidate.pendingOperations,
|
|
4261
|
+
...identityChecked.accepted
|
|
4262
|
+
];
|
|
4263
|
+
candidate.rejectedOperations = identityChecked.rejected;
|
|
4264
|
+
return identityChecked.accepted;
|
|
4265
|
+
};
|
|
4266
|
+
|
|
4172
4267
|
const createBootstrapAttemptOptions = (mode) => ({
|
|
4173
4268
|
mode,
|
|
4174
4269
|
attemptId: `bootstrap:${mode}:${now()}:${++bootstrapAttemptSequence}:${
|
|
@@ -4952,9 +5047,10 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4952
5047
|
}
|
|
4953
5048
|
const applied = applyRemoteBaseline(candidate, baselineValue);
|
|
4954
5049
|
if (applied.stale || !applied.changed) return {success: true, ignored: true};
|
|
5050
|
+
const reconciledOperations = appendVodSiblingReconciliation(candidate, captured);
|
|
4955
5051
|
if (!(await persist(candidate, captured))) return {stale: true, reason: 'stale_session'};
|
|
4956
5052
|
workspace = candidate;
|
|
4957
|
-
notify({type: 'remote_merged'});
|
|
5053
|
+
notify({type: 'remote_merged', operations: clone(reconciledOperations)});
|
|
4958
5054
|
return getSnapshot();
|
|
4959
5055
|
};
|
|
4960
5056
|
|
|
@@ -5322,6 +5418,16 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
5322
5418
|
// Always persist the normalized durable shape before subscribing. This
|
|
5323
5419
|
// also makes legacy migration atomic from the engine's point of view.
|
|
5324
5420
|
clearPersistedHydrationBarrier(candidate);
|
|
5421
|
+
const reconciledOperations = appendVodSiblingReconciliation(candidate, captured);
|
|
5422
|
+
if (reconciledOperations.length > 0) {
|
|
5423
|
+
const lastOperation = reconciledOperations.at(-1);
|
|
5424
|
+
candidate.syncMeta = {
|
|
5425
|
+
...candidate.syncMeta,
|
|
5426
|
+
lastLocalOperationAt: lastOperation.createdAt,
|
|
5427
|
+
lastLocalOperationId: lastOperation.id,
|
|
5428
|
+
lastLocalOperationType: lastOperation.type
|
|
5429
|
+
};
|
|
5430
|
+
}
|
|
5325
5431
|
if (!(await persist(candidate, captured))) return getSnapshot();
|
|
5326
5432
|
workspace = candidate;
|
|
5327
5433
|
initialized = true;
|
|
@@ -5397,6 +5503,8 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
5397
5503
|
...durableOperations
|
|
5398
5504
|
];
|
|
5399
5505
|
candidate.rejectedOperations = identityChecked.rejected;
|
|
5506
|
+
const reconciledOperations = appendVodSiblingReconciliation(candidate, captured, {syncBatchId});
|
|
5507
|
+
const allDurableOperations = [...durableOperations, ...reconciledOperations];
|
|
5400
5508
|
if (viewOperations.length > 0) {
|
|
5401
5509
|
candidate.syncMeta = {
|
|
5402
5510
|
...candidate.syncMeta,
|
|
@@ -5406,8 +5514,8 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
5406
5514
|
lastLocalViewOperationId: viewOperations[viewOperations.length - 1].id
|
|
5407
5515
|
};
|
|
5408
5516
|
}
|
|
5409
|
-
if (
|
|
5410
|
-
const lastOperation =
|
|
5517
|
+
if (allDurableOperations.length > 0) {
|
|
5518
|
+
const lastOperation = allDurableOperations[allDurableOperations.length - 1];
|
|
5411
5519
|
candidate.syncMeta = {
|
|
5412
5520
|
...candidate.syncMeta,
|
|
5413
5521
|
lastLocalOperationAt: lastOperation.createdAt,
|
|
@@ -5417,11 +5525,11 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
5417
5525
|
}
|
|
5418
5526
|
if (!(await persist(candidate, captured))) return getSnapshot();
|
|
5419
5527
|
workspace = candidate;
|
|
5420
|
-
if (
|
|
5528
|
+
if (allDurableOperations.length > 0 || viewOperations.length > 0) {
|
|
5421
5529
|
notify({
|
|
5422
5530
|
type: 'local_applied',
|
|
5423
|
-
operations: clone([...
|
|
5424
|
-
operation: clone(viewOperations.at(-1) ||
|
|
5531
|
+
operations: clone([...allDurableOperations, ...viewOperations]),
|
|
5532
|
+
operation: clone(viewOperations.at(-1) || allDurableOperations.at(-1)),
|
|
5425
5533
|
rejectedCount: identityChecked.rejectedCount,
|
|
5426
5534
|
deduplicatedCount: identityChecked.deduplicatedCount
|
|
5427
5535
|
});
|
|
@@ -7336,6 +7444,7 @@ export default {
|
|
|
7336
7444
|
extractTwitchVodIdFromUrl,
|
|
7337
7445
|
extractYouTubeVideoIdFromUrl,
|
|
7338
7446
|
buildKnownTwitchVodRecordPatches,
|
|
7447
|
+
buildRecordTimeLabelVodSiblingOperations,
|
|
7339
7448
|
buildRecordTimeLabelVodPatchOperation,
|
|
7340
7449
|
buildRecordTimeLabelVodPatchOperationId,
|
|
7341
7450
|
calculateVodTitleSimilarity,
|
package/src/protocol.js
CHANGED
|
@@ -20,6 +20,11 @@ export const RECORD_TIMELABEL_CAPABILITY_LIFECYCLE_GENERATION_FENCE =
|
|
|
20
20
|
export const RECORD_TIMELABEL_CAPABILITY_CLOUD_FAILURE_STATE = 'cloud-failure-state-v1';
|
|
21
21
|
export const RECORD_TIMELABEL_CAPABILITY_DETERMINISTIC_PLANNER = 'deterministic-planner-v1';
|
|
22
22
|
export const RECORD_TIMELABEL_CAPABILITY_STRICT_READINESS = 'strict-readiness-v1';
|
|
23
|
+
// Platform adapters use this exact code when their local durable store cannot
|
|
24
|
+
// accept another snapshot. Keep provider-specific matching in the adapter;
|
|
25
|
+
// Core owns only the canonical wire code and recovery semantics.
|
|
26
|
+
export const RECORD_TIMELABEL_CLOUD_FAILURE_CODE_LOCAL_STORAGE_QUOTA_EXCEEDED =
|
|
27
|
+
'recordtimelabel_local_storage_quota_exceeded';
|
|
23
28
|
// Keep the original capability spelling for rolling compatibility. New
|
|
24
29
|
// clients may advertise the versioned alias while old clients continue to
|
|
25
30
|
// advertise `lifecycle-generation-fence`.
|
|
@@ -133,6 +138,8 @@ const CLOUD_FAILURE_CODE_CLASSES = new Map([
|
|
|
133
138
|
['too_many_requests', RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES.TRANSIENT],
|
|
134
139
|
['network_error', RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES.TRANSIENT],
|
|
135
140
|
['network_request_failed', RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES.TRANSIENT],
|
|
141
|
+
[RECORD_TIMELABEL_CLOUD_FAILURE_CODE_LOCAL_STORAGE_QUOTA_EXCEEDED,
|
|
142
|
+
RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES.TRANSIENT],
|
|
136
143
|
['bootstrap_required', RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES.BOOTSTRAP_REQUIRED],
|
|
137
144
|
['bootstraprequired', RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES.BOOTSTRAP_REQUIRED],
|
|
138
145
|
['recordtimelabel_bootstrap_required', RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES.BOOTSTRAP_REQUIRED],
|
|
@@ -856,6 +863,7 @@ export const createRecordTimeLabelTransportFailureResults = (
|
|
|
856
863
|
|
|
857
864
|
export default {
|
|
858
865
|
RECORD_TIMELABEL_OPERATION_RESULT_STATUSES,
|
|
866
|
+
RECORD_TIMELABEL_CLOUD_FAILURE_CODE_LOCAL_STORAGE_QUOTA_EXCEEDED,
|
|
859
867
|
RECORD_TIMELABEL_CAPABILITY_OPERATION_CONFLICT_QUARANTINE,
|
|
860
868
|
RECORD_TIMELABEL_CAPABILITY_STRICT_OPERATION_RESULTS,
|
|
861
869
|
RECORD_TIMELABEL_CAPABILITY_LIFECYCLE_GENERATION_FENCE,
|