@recordtimelabel/core 0.3.0 → 0.3.1
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 +7 -1
- package/package.json +1 -1
- package/src/index.js +77 -10
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ During local development an app can consume a sibling checkout with:
|
|
|
20
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 v2 gateway contract release is:
|
|
21
21
|
|
|
22
22
|
```json
|
|
23
|
-
"@recordtimelabel/core": "0.3.
|
|
23
|
+
"@recordtimelabel/core": "0.3.1"
|
|
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.
|
|
@@ -83,6 +83,12 @@ The core also normalizes and preserves these compatibility metadata fields:
|
|
|
83
83
|
|
|
84
84
|
Deletion must be represented by tombstones or operations. Do not reintroduce the old heuristic that treats "local exists but cloud missing for more than five minutes" as deletion.
|
|
85
85
|
|
|
86
|
+
`mergeLocalRemote` resolves deletions by `lifecycleGeneration`, in both directions: a tombstone or
|
|
87
|
+
trash entry is dropped when either side carries that entity as active with a higher generation,
|
|
88
|
+
which is what restore operations bump. A stale snapshot on either side therefore cannot re-delete
|
|
89
|
+
an entity that was already restored, and consumers must not reimplement this reconciliation in an
|
|
90
|
+
app-level wrapper.
|
|
91
|
+
|
|
86
92
|
## Firestore v2 Gateway Contract
|
|
87
93
|
|
|
88
94
|
The package exposes platform-neutral v2 document helpers so app adapters can share the same record/folder/settings/ops shape before wiring Firebase SDK or REST calls:
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@ const REQUIRED_FOLDERS = [
|
|
|
5
5
|
{ id: DEFAULT_FOLDER_ID, name: 'Uncategorized' }
|
|
6
6
|
];
|
|
7
7
|
|
|
8
|
-
export const RECORD_TIMELABEL_CORE_VERSION = '0.3.
|
|
8
|
+
export const RECORD_TIMELABEL_CORE_VERSION = '0.3.1';
|
|
9
9
|
export const RTL_SYNC_PROTOCOL_VERSION = 2;
|
|
10
10
|
export const RTL_MAX_OPERATIONS_PER_REQUEST = 20;
|
|
11
11
|
export const RTL_MAX_REQUEST_BYTES = 256 * 1024;
|
|
@@ -241,6 +241,66 @@ const mergeTrashEntries = (remoteEntries = {}, localEntries = {}) => {
|
|
|
241
241
|
return merged;
|
|
242
242
|
};
|
|
243
243
|
|
|
244
|
+
const collectActiveLifecycleGenerations = (...states) => {
|
|
245
|
+
const records = new Map();
|
|
246
|
+
const folders = new Map();
|
|
247
|
+
const track = (target, id, entity) => {
|
|
248
|
+
if (!id) return;
|
|
249
|
+
const generation = Number(entity?.lifecycleGeneration || 0);
|
|
250
|
+
target.set(id, Math.max(generation, target.get(id) || 0));
|
|
251
|
+
};
|
|
252
|
+
states.forEach((state) => {
|
|
253
|
+
Object.entries(state?.records || {}).forEach(([folderId, folderRecords]) => {
|
|
254
|
+
if (VIRTUAL_FOLDER_IDS.has(normalizeId(folderId))) return;
|
|
255
|
+
toArray(folderRecords).forEach((record) => track(records, getRecordId(record), record));
|
|
256
|
+
});
|
|
257
|
+
toArray(state?.folders).forEach((folder) => track(folders, normalizeId(folder?.id), folder));
|
|
258
|
+
});
|
|
259
|
+
return { records, folders };
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* A tombstone or trash entry only stays valid while it describes the newest lifecycle event for
|
|
264
|
+
* its entity. Once either side carries that entity as active with a higher lifecycleGeneration —
|
|
265
|
+
* which restore operations bump — the deletion is stale and must be dropped, otherwise
|
|
266
|
+
* mergeTombstones' union lets a stale snapshot re-delete a record that was already restored.
|
|
267
|
+
* Generations default to 0, so legacy data never outranks a real tombstone.
|
|
268
|
+
*/
|
|
269
|
+
const dropSupersededLifecycleDeletions = ({
|
|
270
|
+
deletedRecordTombstones = {},
|
|
271
|
+
deletedFolderTombstones = {},
|
|
272
|
+
trashEntries = {},
|
|
273
|
+
activeGenerations
|
|
274
|
+
}) => {
|
|
275
|
+
const isSuperseded = (target, id, deletion) => (
|
|
276
|
+
(target.get(normalizeId(id)) || 0) > Number(deletion?.lifecycleGeneration || 0)
|
|
277
|
+
);
|
|
278
|
+
const nextRecordTombstones = { ...deletedRecordTombstones };
|
|
279
|
+
Object.entries(nextRecordTombstones).forEach(([recordId, tombstone]) => {
|
|
280
|
+
if (isSuperseded(activeGenerations.records, recordId, tombstone)) {
|
|
281
|
+
delete nextRecordTombstones[recordId];
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
const nextFolderTombstones = { ...deletedFolderTombstones };
|
|
285
|
+
Object.entries(nextFolderTombstones).forEach(([folderId, tombstone]) => {
|
|
286
|
+
if (isSuperseded(activeGenerations.folders, folderId, tombstone)) {
|
|
287
|
+
delete nextFolderTombstones[folderId];
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
const nextTrashEntries = { ...trashEntries };
|
|
291
|
+
Object.values(nextTrashEntries).forEach((entry) => {
|
|
292
|
+
const target = entry?.kind === 'folder' ? activeGenerations.folders : activeGenerations.records;
|
|
293
|
+
if (isSuperseded(target, entry?.entityId, entry)) {
|
|
294
|
+
delete nextTrashEntries[entry.id];
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
return {
|
|
298
|
+
deletedRecordTombstones: nextRecordTombstones,
|
|
299
|
+
deletedFolderTombstones: nextFolderTombstones,
|
|
300
|
+
trashEntries: nextTrashEntries
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
|
|
244
304
|
const resolveRecordFolderId = (folderId, record = {}) => {
|
|
245
305
|
const candidates = [
|
|
246
306
|
record.folderId,
|
|
@@ -1060,15 +1120,22 @@ export const mergeLocalRemote = ({
|
|
|
1060
1120
|
} = {}) => {
|
|
1061
1121
|
const local = normalizeState(localState);
|
|
1062
1122
|
const remote = normalizeState(remoteState);
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1123
|
+
const {
|
|
1124
|
+
deletedRecordTombstones,
|
|
1125
|
+
deletedFolderTombstones,
|
|
1126
|
+
trashEntries
|
|
1127
|
+
} = dropSupersededLifecycleDeletions({
|
|
1128
|
+
deletedRecordTombstones: mergeTombstones(
|
|
1129
|
+
remote.deletedRecordTombstones,
|
|
1130
|
+
local.deletedRecordTombstones
|
|
1131
|
+
),
|
|
1132
|
+
deletedFolderTombstones: mergeTombstones(
|
|
1133
|
+
remote.deletedFolderTombstones,
|
|
1134
|
+
local.deletedFolderTombstones
|
|
1135
|
+
),
|
|
1136
|
+
trashEntries: mergeTrashEntries(remote.trashEntries, local.trashEntries),
|
|
1137
|
+
activeGenerations: collectActiveLifecycleGenerations(remote, local)
|
|
1138
|
+
});
|
|
1072
1139
|
const folders = mergeFolders(remote.folders, local.folders, deletedFolderTombstones);
|
|
1073
1140
|
const folderIds = new Set(ensureFolders(folders).map((folder) => folder.id));
|
|
1074
1141
|
|