@recordtimelabel/core 0.6.5 → 0.6.7

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 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.5`; verify that its registry tarball and lockfile integrity are available before updating consumers:
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.7`; verify that its registry tarball and lockfile integrity are available before updating consumers:
21
21
 
22
22
  ```json
23
- "@recordtimelabel/core": "0.6.5"
23
+ "@recordtimelabel/core": "0.6.7"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recordtimelabel/core",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "type": "module",
5
5
  "description": "Shared RecordTimeLabel data model, merge logic, operations, and sync engine.",
6
6
  "main": "./src/index.js",
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.5';
148
+ export const RECORD_TIMELABEL_CORE_VERSION = '0.6.7';
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',
@@ -1145,6 +1149,10 @@ export const applyOperation = (state = {}, operation = {}) => {
1145
1149
  originalRecordIndex: Number.isInteger(payload.originalRecordIndex)
1146
1150
  ? payload.originalRecordIndex
1147
1151
  : Math.max(0, Number(existingEntry?.index || 0)),
1152
+ originalGroupId: String(payload.originalGroupId || '').trim(),
1153
+ originalGroupOrderIndex: Number.isInteger(payload.originalGroupOrderIndex)
1154
+ ? payload.originalGroupOrderIndex
1155
+ : -1,
1148
1156
  lifecycleGeneration,
1149
1157
  deletedAt,
1150
1158
  purgeAt: toFiniteTimestamp(payload.purgeAt) || deletedAt + RTL_TRASH_RETENTION_MS,
@@ -1206,6 +1214,16 @@ export const applyOperation = (state = {}, operation = {}) => {
1206
1214
  );
1207
1215
  targetRecords.splice(restoreIndex, 0, restoredRecord);
1208
1216
  nextState.records[targetFolderId] = targetRecords;
1217
+ const originalGroupId = String(trashEntry.originalGroupId || '').trim();
1218
+ const originalGroupOrderIndex = Number.isInteger(trashEntry.originalGroupOrderIndex)
1219
+ ? trashEntry.originalGroupOrderIndex
1220
+ : -1;
1221
+ if (originalGroupId && originalGroupOrderIndex >= 0 &&
1222
+ !nextState.groupOrder.includes(originalGroupId)) {
1223
+ const groupOrder = [...nextState.groupOrder];
1224
+ groupOrder.splice(Math.min(originalGroupOrderIndex, groupOrder.length), 0, originalGroupId);
1225
+ nextState.groupOrder = groupOrder;
1226
+ }
1209
1227
  delete nextState.trashEntries[trashEntryId];
1210
1228
  delete nextState.deletedRecordTombstones[recordId];
1211
1229
  break;
@@ -6232,6 +6250,19 @@ const rtlPayloadOrder = (operation, keys) => {
6232
6250
  }
6233
6251
  return [];
6234
6252
  };
6253
+ // Partial delete reads omit sibling records, so folder.recordOrder is the live slot.
6254
+ const rtlResolveOriginalRecordIndex = (payload, existingRecord, folders) => {
6255
+ if (Number.isInteger(payload?.originalRecordIndex) && payload.originalRecordIndex >= 0) {
6256
+ return payload.originalRecordIndex;
6257
+ }
6258
+ const recordId = normalizeId(existingRecord?.id);
6259
+ const folderId = safeFolderId(existingRecord?.folderId || payload?.folderId);
6260
+ const recordOrder = rtlMergeOrder(
6261
+ rtlOwn(folders, folderId) ? folders[folderId]?.recordOrder : []
6262
+ );
6263
+ const orderIndex = recordId ? recordOrder.indexOf(recordId) : -1;
6264
+ return orderIndex >= 0 ? orderIndex : 0;
6265
+ };
6235
6266
  const rtlHasInvalidPayloadValue = (value, depth = 0) => {
6236
6267
  if (depth > 20) return true;
6237
6268
  if (typeof value === 'number') return !Number.isFinite(value);
@@ -6863,7 +6894,21 @@ export const planFirestoreV2OperationChanges = ({
6863
6894
  break;
6864
6895
  }
6865
6896
  const sourceFolderId = safeFolderId(existingRecord.folderId);
6866
- applied = rtlApplyOperationToPartialDocuments({...nextDocuments, operation, now: operationNow});
6897
+ applied = rtlApplyOperationToPartialDocuments({
6898
+ ...nextDocuments,
6899
+ operation: {
6900
+ ...operation,
6901
+ payload: {
6902
+ ...payload,
6903
+ originalRecordIndex: rtlResolveOriginalRecordIndex(
6904
+ payload,
6905
+ existingRecord,
6906
+ nextDocuments.folders
6907
+ )
6908
+ }
6909
+ },
6910
+ now: operationNow
6911
+ });
6867
6912
  rtlSetRoot(nextDocuments.root, applied.documents.root);
6868
6913
  const trashDocument = rtlOwn(applied.documents.trash, operationTrashEntryId)
6869
6914
  ? applied.documents.trash[operationTrashEntryId]
@@ -7286,8 +7331,10 @@ export default {
7286
7331
  RECORD_TIMELABEL_CAPABILITY_STRICT_READINESS,
7287
7332
  RECORD_TIMELABEL_CLOUD_FAILURE_SCHEMA_VERSION,
7288
7333
  RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES,
7334
+ RECORD_TIMELABEL_CLOUD_RECOVERY_ACTIONS,
7289
7335
  isRecordTimeLabelOperationConflictFailure,
7290
7336
  normalizeRecordTimeLabelCloudFailure,
7337
+ classifyRecordTimeLabelCloudRecovery,
7291
7338
  toRecordTimeLabelCloudFailureError,
7292
7339
  RECORD_TIMELABEL_OPERATION_RESULT_STATUSES,
7293
7340
  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,