@recordtimelabel/core 0.6.9 → 0.6.10
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/domain.js +1 -0
- package/src/index.js +119 -12
package/package.json
CHANGED
package/src/domain.js
CHANGED
package/src/index.js
CHANGED
|
@@ -145,13 +145,14 @@ export {
|
|
|
145
145
|
selectBestMatchingTwitchVod
|
|
146
146
|
};
|
|
147
147
|
|
|
148
|
-
export const RECORD_TIMELABEL_CORE_VERSION = '0.6.
|
|
148
|
+
export const RECORD_TIMELABEL_CORE_VERSION = '0.6.10';
|
|
149
149
|
export const RTL_SYNC_PROTOCOL_VERSION = 2;
|
|
150
150
|
export const RECORD_TIMELABEL_DURABLE_ENGINE_CAPABILITIES = Object.freeze([
|
|
151
151
|
'fifo-retry-fence',
|
|
152
152
|
'baseline-refresh-after-rejection',
|
|
153
153
|
'sync-batch-boundary',
|
|
154
154
|
'remote-subscription-readiness',
|
|
155
|
+
'twitch-vod-sibling-reconciliation-v1',
|
|
155
156
|
RECORD_TIMELABEL_CAPABILITY_OPERATION_CONFLICT_QUARANTINE,
|
|
156
157
|
RECORD_TIMELABEL_CAPABILITY_STRICT_OPERATION_RESULTS,
|
|
157
158
|
RECORD_TIMELABEL_CAPABILITY_LIFECYCLE_GENERATION_FENCE,
|
|
@@ -1484,6 +1485,27 @@ export const applyOperation = (state = {}, operation = {}) => {
|
|
|
1484
1485
|
|
|
1485
1486
|
export const applyRecordTimeLabelOperation = (state = {}, operation = {}) => applyOperation(state, operation);
|
|
1486
1487
|
|
|
1488
|
+
const collectRecordTimeLabelDomainRecords = (records = {}) => Object.entries(records || {})
|
|
1489
|
+
.flatMap(([folderId, folderRecords]) => (
|
|
1490
|
+
folderId === 'all' || !Array.isArray(folderRecords) ? [] : folderRecords
|
|
1491
|
+
));
|
|
1492
|
+
|
|
1493
|
+
export const buildRecordTimeLabelVodSiblingOperations = ({
|
|
1494
|
+
state = {},
|
|
1495
|
+
fallbackPlatform = 'unknown',
|
|
1496
|
+
unknownTitle
|
|
1497
|
+
} = {}) => {
|
|
1498
|
+
const normalized = normalizeState(state);
|
|
1499
|
+
const result = buildKnownTwitchVodRecordPatches({
|
|
1500
|
+
records: collectRecordTimeLabelDomainRecords(normalized.records),
|
|
1501
|
+
fallbackPlatform,
|
|
1502
|
+
unknownTitle
|
|
1503
|
+
});
|
|
1504
|
+
return result.patches.map(({recordId, patch}) => (
|
|
1505
|
+
buildRecordTimeLabelVodPatchOperation({recordId, patch})
|
|
1506
|
+
));
|
|
1507
|
+
};
|
|
1508
|
+
|
|
1487
1509
|
export const mergeLocalRemote = ({
|
|
1488
1510
|
localState = {},
|
|
1489
1511
|
remoteState = {},
|
|
@@ -3028,6 +3050,22 @@ export const createRecordTimeLabelController = ({
|
|
|
3028
3050
|
});
|
|
3029
3051
|
};
|
|
3030
3052
|
|
|
3053
|
+
const reconcileVodSiblings = () => {
|
|
3054
|
+
const pendingIds = new Set(pendingOps.map((operation) => operation?.id).filter(Boolean));
|
|
3055
|
+
const operations = buildRecordTimeLabelVodSiblingOperations({state})
|
|
3056
|
+
.filter((operation) => !pendingIds.has(operation.id))
|
|
3057
|
+
.map((operation) => ({
|
|
3058
|
+
...operation,
|
|
3059
|
+
clientId: operation.clientId || clientId,
|
|
3060
|
+
createdAt: operation.createdAt || clock()
|
|
3061
|
+
}));
|
|
3062
|
+
operations.forEach((operation) => {
|
|
3063
|
+
state = applyRecordTimeLabelOperation(state, operation);
|
|
3064
|
+
});
|
|
3065
|
+
if (operations.length > 0) pendingOps = [...pendingOps, ...operations];
|
|
3066
|
+
return operations;
|
|
3067
|
+
};
|
|
3068
|
+
|
|
3031
3069
|
const controller = {
|
|
3032
3070
|
async init() {
|
|
3033
3071
|
const loaded = await storageAdapter.load(storageKeys);
|
|
@@ -3035,6 +3073,8 @@ export const createRecordTimeLabelController = ({
|
|
|
3035
3073
|
pendingOps = toArray(loaded?.pendingOps).map((operation) => ({ ...operation }));
|
|
3036
3074
|
syncMeta = loaded?.syncMeta && typeof loaded.syncMeta === 'object' ? { ...loaded.syncMeta } : {};
|
|
3037
3075
|
|
|
3076
|
+
let shouldPersist = false;
|
|
3077
|
+
|
|
3038
3078
|
if (cloudAdapter?.load) {
|
|
3039
3079
|
const remoteState = await cloudAdapter.load();
|
|
3040
3080
|
const merged = mergeRemoteStateIntoLocal({
|
|
@@ -3047,8 +3087,20 @@ export const createRecordTimeLabelController = ({
|
|
|
3047
3087
|
now: clock()
|
|
3048
3088
|
});
|
|
3049
3089
|
state = merged.state;
|
|
3050
|
-
|
|
3090
|
+
shouldPersist = true;
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
const reconciledOperations = reconcileVodSiblings();
|
|
3094
|
+
if (reconciledOperations.length > 0) {
|
|
3095
|
+
const lastOperation = reconciledOperations.at(-1);
|
|
3096
|
+
syncMeta = {
|
|
3097
|
+
...syncMeta,
|
|
3098
|
+
lastLocalOperationAt: lastOperation.createdAt,
|
|
3099
|
+
lastLocalOperationType: lastOperation.type
|
|
3100
|
+
};
|
|
3101
|
+
shouldPersist = true;
|
|
3051
3102
|
}
|
|
3103
|
+
if (shouldPersist) await persist();
|
|
3052
3104
|
|
|
3053
3105
|
if (cloudAdapter?.subscribe) {
|
|
3054
3106
|
unsubscribeCloud = cloudAdapter.subscribe(async (remoteState) => {
|
|
@@ -3079,13 +3131,19 @@ export const createRecordTimeLabelController = ({
|
|
|
3079
3131
|
};
|
|
3080
3132
|
state = applyRecordTimeLabelOperation(state, nextOperation);
|
|
3081
3133
|
pendingOps = [...pendingOps, nextOperation];
|
|
3134
|
+
const reconciledOperations = reconcileVodSiblings();
|
|
3135
|
+
const lastOperation = reconciledOperations.at(-1) || nextOperation;
|
|
3082
3136
|
syncMeta = {
|
|
3083
3137
|
...syncMeta,
|
|
3084
|
-
lastLocalOperationAt:
|
|
3085
|
-
lastLocalOperationType:
|
|
3138
|
+
lastLocalOperationAt: lastOperation.createdAt,
|
|
3139
|
+
lastLocalOperationType: lastOperation.type
|
|
3086
3140
|
};
|
|
3087
3141
|
await persist();
|
|
3088
|
-
notify({
|
|
3142
|
+
notify({
|
|
3143
|
+
type: 'local_applied',
|
|
3144
|
+
operation: nextOperation,
|
|
3145
|
+
operations: [nextOperation, ...reconciledOperations]
|
|
3146
|
+
});
|
|
3089
3147
|
return getSnapshot();
|
|
3090
3148
|
},
|
|
3091
3149
|
|
|
@@ -3100,8 +3158,17 @@ export const createRecordTimeLabelController = ({
|
|
|
3100
3158
|
now: clock()
|
|
3101
3159
|
});
|
|
3102
3160
|
state = merged.state;
|
|
3161
|
+
const reconciledOperations = reconcileVodSiblings();
|
|
3162
|
+
if (reconciledOperations.length > 0) {
|
|
3163
|
+
const lastOperation = reconciledOperations.at(-1);
|
|
3164
|
+
syncMeta = {
|
|
3165
|
+
...syncMeta,
|
|
3166
|
+
lastLocalOperationAt: lastOperation.createdAt,
|
|
3167
|
+
lastLocalOperationType: lastOperation.type
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
3103
3170
|
await persist();
|
|
3104
|
-
notify({ type: 'remote_merged' });
|
|
3171
|
+
notify({ type: 'remote_merged', operations: reconciledOperations });
|
|
3105
3172
|
return getSnapshot();
|
|
3106
3173
|
},
|
|
3107
3174
|
|
|
@@ -4169,6 +4236,32 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4169
4236
|
return {changed, stale: false};
|
|
4170
4237
|
};
|
|
4171
4238
|
|
|
4239
|
+
const appendVodSiblingReconciliation = (candidate, captured, {syncBatchId = null} = {}) => {
|
|
4240
|
+
const operations = buildRecordTimeLabelVodSiblingOperations({
|
|
4241
|
+
state: rtlDeriveDurableVisibleState(candidate)
|
|
4242
|
+
});
|
|
4243
|
+
if (operations.length === 0) return [];
|
|
4244
|
+
|
|
4245
|
+
const normalized = operations.map((operation) => rtlNormalizePendingOperation(operation, {
|
|
4246
|
+
client,
|
|
4247
|
+
clientId: typeof client === 'string' ? client : client?.id,
|
|
4248
|
+
now,
|
|
4249
|
+
ownerUid: captured?.uid ?? candidate.ownerUid,
|
|
4250
|
+
workspaceEpoch: captured?.workspaceEpoch ?? candidate.workspaceEpoch
|
|
4251
|
+
}));
|
|
4252
|
+
const reconciliationBatchId = syncBatchId || rtlStableSyncBatchId(normalized);
|
|
4253
|
+
normalized.forEach((operation) => {
|
|
4254
|
+
if (!operation.syncBatchId) operation.syncBatchId = reconciliationBatchId;
|
|
4255
|
+
});
|
|
4256
|
+
const identityChecked = rtlQuarantineOperations(candidate, normalized, now());
|
|
4257
|
+
candidate.pendingOperations = [
|
|
4258
|
+
...candidate.pendingOperations,
|
|
4259
|
+
...identityChecked.accepted
|
|
4260
|
+
];
|
|
4261
|
+
candidate.rejectedOperations = identityChecked.rejected;
|
|
4262
|
+
return identityChecked.accepted;
|
|
4263
|
+
};
|
|
4264
|
+
|
|
4172
4265
|
const createBootstrapAttemptOptions = (mode) => ({
|
|
4173
4266
|
mode,
|
|
4174
4267
|
attemptId: `bootstrap:${mode}:${now()}:${++bootstrapAttemptSequence}:${
|
|
@@ -4952,9 +5045,10 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4952
5045
|
}
|
|
4953
5046
|
const applied = applyRemoteBaseline(candidate, baselineValue);
|
|
4954
5047
|
if (applied.stale || !applied.changed) return {success: true, ignored: true};
|
|
5048
|
+
const reconciledOperations = appendVodSiblingReconciliation(candidate, captured);
|
|
4955
5049
|
if (!(await persist(candidate, captured))) return {stale: true, reason: 'stale_session'};
|
|
4956
5050
|
workspace = candidate;
|
|
4957
|
-
notify({type: 'remote_merged'});
|
|
5051
|
+
notify({type: 'remote_merged', operations: clone(reconciledOperations)});
|
|
4958
5052
|
return getSnapshot();
|
|
4959
5053
|
};
|
|
4960
5054
|
|
|
@@ -5322,6 +5416,16 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
5322
5416
|
// Always persist the normalized durable shape before subscribing. This
|
|
5323
5417
|
// also makes legacy migration atomic from the engine's point of view.
|
|
5324
5418
|
clearPersistedHydrationBarrier(candidate);
|
|
5419
|
+
const reconciledOperations = appendVodSiblingReconciliation(candidate, captured);
|
|
5420
|
+
if (reconciledOperations.length > 0) {
|
|
5421
|
+
const lastOperation = reconciledOperations.at(-1);
|
|
5422
|
+
candidate.syncMeta = {
|
|
5423
|
+
...candidate.syncMeta,
|
|
5424
|
+
lastLocalOperationAt: lastOperation.createdAt,
|
|
5425
|
+
lastLocalOperationId: lastOperation.id,
|
|
5426
|
+
lastLocalOperationType: lastOperation.type
|
|
5427
|
+
};
|
|
5428
|
+
}
|
|
5325
5429
|
if (!(await persist(candidate, captured))) return getSnapshot();
|
|
5326
5430
|
workspace = candidate;
|
|
5327
5431
|
initialized = true;
|
|
@@ -5397,6 +5501,8 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
5397
5501
|
...durableOperations
|
|
5398
5502
|
];
|
|
5399
5503
|
candidate.rejectedOperations = identityChecked.rejected;
|
|
5504
|
+
const reconciledOperations = appendVodSiblingReconciliation(candidate, captured, {syncBatchId});
|
|
5505
|
+
const allDurableOperations = [...durableOperations, ...reconciledOperations];
|
|
5400
5506
|
if (viewOperations.length > 0) {
|
|
5401
5507
|
candidate.syncMeta = {
|
|
5402
5508
|
...candidate.syncMeta,
|
|
@@ -5406,8 +5512,8 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
5406
5512
|
lastLocalViewOperationId: viewOperations[viewOperations.length - 1].id
|
|
5407
5513
|
};
|
|
5408
5514
|
}
|
|
5409
|
-
if (
|
|
5410
|
-
const lastOperation =
|
|
5515
|
+
if (allDurableOperations.length > 0) {
|
|
5516
|
+
const lastOperation = allDurableOperations[allDurableOperations.length - 1];
|
|
5411
5517
|
candidate.syncMeta = {
|
|
5412
5518
|
...candidate.syncMeta,
|
|
5413
5519
|
lastLocalOperationAt: lastOperation.createdAt,
|
|
@@ -5417,11 +5523,11 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
5417
5523
|
}
|
|
5418
5524
|
if (!(await persist(candidate, captured))) return getSnapshot();
|
|
5419
5525
|
workspace = candidate;
|
|
5420
|
-
if (
|
|
5526
|
+
if (allDurableOperations.length > 0 || viewOperations.length > 0) {
|
|
5421
5527
|
notify({
|
|
5422
5528
|
type: 'local_applied',
|
|
5423
|
-
operations: clone([...
|
|
5424
|
-
operation: clone(viewOperations.at(-1) ||
|
|
5529
|
+
operations: clone([...allDurableOperations, ...viewOperations]),
|
|
5530
|
+
operation: clone(viewOperations.at(-1) || allDurableOperations.at(-1)),
|
|
5425
5531
|
rejectedCount: identityChecked.rejectedCount,
|
|
5426
5532
|
deduplicatedCount: identityChecked.deduplicatedCount
|
|
5427
5533
|
});
|
|
@@ -7336,6 +7442,7 @@ export default {
|
|
|
7336
7442
|
extractTwitchVodIdFromUrl,
|
|
7337
7443
|
extractYouTubeVideoIdFromUrl,
|
|
7338
7444
|
buildKnownTwitchVodRecordPatches,
|
|
7445
|
+
buildRecordTimeLabelVodSiblingOperations,
|
|
7339
7446
|
buildRecordTimeLabelVodPatchOperation,
|
|
7340
7447
|
buildRecordTimeLabelVodPatchOperationId,
|
|
7341
7448
|
calculateVodTitleSimilarity,
|