@recordtimelabel/core 0.1.7 → 0.1.9

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.
Files changed (3) hide show
  1. package/README.md +7 -4
  2. package/package.json +1 -1
  3. package/src/index.js +124 -32
package/README.md CHANGED
@@ -17,11 +17,13 @@ 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 current release candidate is:
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 current published release is:
21
21
 
22
22
  ```json
23
- "@recordtimelabel/core": "0.1.2"
23
+ "@recordtimelabel/core": "0.1.9"
24
24
  ```
25
+
26
+ If this checkout's `package.json` is ahead of the published version, publish the new package before updating consumers to that version.
25
27
 
26
28
  ## Public API
27
29
 
@@ -32,8 +34,8 @@ For release builds, consume a fixed npm package, git tag, or private registry ve
32
34
  - `applyRecordTimeLabelOperation(state, operation)`
33
35
  - `createSyncEngine({ storageAdapter, cloudAdapter, clientId, clock, logger })`
34
36
  - `createRecordTimeLabelController({ storageAdapter, cloudAdapter, clientId, settingKeys, clock, logger })`
35
- - `buildRecordTimeLabelSyncPayload({ data, previousCloudData, pendingOps, syncMode, clientId, now })`
36
- - `buildSyncPayloadWithOperations({ data, previousCloudData, pendingOps, syncMode, clientId, now })`
37
+ - `buildRecordTimeLabelSyncPayload({ data, previousCloudData, pendingOps, syncMode, clientId, now, groupOrderNormalizer })`
38
+ - `buildSyncPayloadWithOperations({ data, previousCloudData, pendingOps, syncMode, clientId, now, groupOrderNormalizer })`
37
39
  - `flushPendingOperations({ operations, storageAdapter, cloudAdapter, clientId, now })`
38
40
  - `mergeRemoteStateIntoLocal({ localData, remoteData, pendingOps, settingKeys, clientId, now })`
39
41
  - `buildLocalStateFromStorage(data, options)`
@@ -41,6 +43,7 @@ For release builds, consume a fixed npm package, git tag, or private registry ve
41
43
  - `buildFirestoreV1UserPatch(state, options)`
42
44
  - `buildFirestoreV2LogicalPaths(userId)`
43
45
  - `buildFirestoreV2DocumentsFromState(state, options)`
46
+ - `buildFirestoreV2DocumentChangeSet(previousDocuments, nextDocuments, options)`
44
47
  - `buildStateFromFirestoreV2Documents(documents, options)`
45
48
  - `hasMeaningfulRecordTimeLabelCloudState(data, options)`
46
49
  - `buildRecordTimeLabelContentFingerprint(data)`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recordtimelabel/core",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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
@@ -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.1.6';
8
+ export const RECORD_TIMELABEL_CORE_VERSION = '0.1.9';
9
9
 
10
10
  export const OPERATION_TYPES = Object.freeze({
11
11
  RECORD_CREATE: 'record.create',
@@ -1274,6 +1274,94 @@ export const buildFirestoreV2DocumentsFromState = (state = {}, options = {}) =>
1274
1274
  };
1275
1275
  };
1276
1276
 
1277
+ const canonicalizeFirestoreV2DocumentValue = (value) => {
1278
+ if (Array.isArray(value)) {
1279
+ return value.map((entry) => canonicalizeFirestoreV2DocumentValue(entry));
1280
+ }
1281
+ if (value instanceof Date) {
1282
+ return value.toISOString();
1283
+ }
1284
+ if (!value || typeof value !== 'object') {
1285
+ return value;
1286
+ }
1287
+
1288
+ return Object.keys(value)
1289
+ .sort()
1290
+ .reduce((result, key) => {
1291
+ result[key] = canonicalizeFirestoreV2DocumentValue(value[key]);
1292
+ return result;
1293
+ }, {});
1294
+ };
1295
+
1296
+ const areFirestoreV2DocumentValuesEqual = (left, right) => (
1297
+ JSON.stringify(canonicalizeFirestoreV2DocumentValue(left)) ===
1298
+ JSON.stringify(canonicalizeFirestoreV2DocumentValue(right))
1299
+ );
1300
+
1301
+ const buildFirestoreV2CollectionChangeSet = (previousCollection, nextCollection, allowDeletes) => {
1302
+ const previous = previousCollection && typeof previousCollection === 'object'
1303
+ ? previousCollection
1304
+ : {};
1305
+ const next = nextCollection && typeof nextCollection === 'object'
1306
+ ? nextCollection
1307
+ : {};
1308
+ const upserts = {};
1309
+
1310
+ Object.keys(next).sort().forEach((documentId) => {
1311
+ if (!Object.prototype.hasOwnProperty.call(previous, documentId) ||
1312
+ !areFirestoreV2DocumentValuesEqual(previous[documentId], next[documentId])) {
1313
+ upserts[documentId] = next[documentId];
1314
+ }
1315
+ });
1316
+
1317
+ const deleteIds = allowDeletes
1318
+ ? Object.keys(previous).filter((documentId) => (
1319
+ !Object.prototype.hasOwnProperty.call(next, documentId)
1320
+ )).sort()
1321
+ : [];
1322
+
1323
+ return { upserts, deleteIds };
1324
+ };
1325
+
1326
+ export const buildFirestoreV2DocumentChangeSet = (
1327
+ previousDocuments = {},
1328
+ nextDocuments = {},
1329
+ options = {}
1330
+ ) => {
1331
+ const allowDeletes = options.allowDeletes === true;
1332
+ const previousRoot = previousDocuments?.root;
1333
+ const nextRoot = nextDocuments?.root;
1334
+ const rootUpsert = nextRoot && !areFirestoreV2DocumentValuesEqual(previousRoot, nextRoot)
1335
+ ? nextRoot
1336
+ : null;
1337
+ const records = buildFirestoreV2CollectionChangeSet(
1338
+ previousDocuments?.records,
1339
+ nextDocuments?.records,
1340
+ allowDeletes
1341
+ );
1342
+ const folders = buildFirestoreV2CollectionChangeSet(
1343
+ previousDocuments?.folders,
1344
+ nextDocuments?.folders,
1345
+ allowDeletes
1346
+ );
1347
+ const ops = buildFirestoreV2CollectionChangeSet(
1348
+ previousDocuments?.ops,
1349
+ nextDocuments?.ops,
1350
+ allowDeletes
1351
+ );
1352
+ const hasCollectionChanges = [records, folders, ops].some((changeSet) => (
1353
+ Object.keys(changeSet.upserts).length > 0 || changeSet.deleteIds.length > 0
1354
+ ));
1355
+
1356
+ return {
1357
+ hasChanges: Boolean(rootUpsert) || hasCollectionChanges,
1358
+ root: { upsert: rootUpsert },
1359
+ records,
1360
+ folders,
1361
+ ops
1362
+ };
1363
+ };
1364
+
1277
1365
  const removeV2Metadata = (data = {}) => {
1278
1366
  const { schemaVersion, folderId, ...rest } = data || {};
1279
1367
  return rest;
@@ -1502,14 +1590,14 @@ const resolveMergedOrder = ({
1502
1590
  return mergeOrderArrays(normalizer, remoteData?.[stateKey], localData?.[stateKey]);
1503
1591
  };
1504
1592
 
1505
- const buildSyncStateInput = (data = {}, operationTime) => ({
1506
- folders: data?.folders || [],
1507
- records: data?.records || {},
1508
- settings: data?.settings || {},
1509
- groupOrder: data?.groupOrder || [],
1510
- folderOrder: data?.folderOrder || [],
1511
- expandedGroups: data?.expandedGroups || [],
1512
- rtlSyncMeta: data?.rtlSyncMeta || {},
1593
+ const buildSyncStateInput = (data = {}, operationTime, options = {}) => ({
1594
+ folders: data?.folders || [],
1595
+ records: data?.records || {},
1596
+ settings: data?.settings || {},
1597
+ groupOrder: normalizeGroupOrderForOption(data?.groupOrder || [], options.groupOrderNormalizer),
1598
+ folderOrder: data?.folderOrder || [],
1599
+ expandedGroups: data?.expandedGroups || [],
1600
+ rtlSyncMeta: data?.rtlSyncMeta || {},
1513
1601
  deletedRecordTombstones: data?.deletedRecordTombstones || {},
1514
1602
  deletedFolderTombstones: data?.deletedFolderTombstones || {},
1515
1603
  lastModified: operationTime
@@ -1518,23 +1606,24 @@ const buildSyncStateInput = (data = {}, operationTime) => ({
1518
1606
  export const buildRecordTimeLabelSyncPayload = ({
1519
1607
  data,
1520
1608
  previousCloudData,
1521
- pendingOps = [],
1522
- syncMode = RECORD_TIMELABEL_SYNC_MODES.FULL,
1523
- clientId = 'recordtimelabel-client',
1524
- now = Date.now()
1525
- } = {}) => {
1526
- const operationTime = toFiniteTimestamp(data?.lastModified) || now;
1527
- const payloadSource = syncMode === RECORD_TIMELABEL_SYNC_MODES.MERGE
1528
- ? mergeLocalRemote({
1529
- localState: buildSyncStateInput(data || {}, operationTime),
1530
- remoteState: previousCloudData || {},
1531
- pendingOps,
1532
- clientId,
1533
- now
1534
- }).state
1535
- : buildSyncStateInput(data || {}, operationTime);
1536
- const sanitizedRecords = removeRecordSyncFlags(payloadSource.records || {});
1537
- const deletionTombstones = buildDeletionTombstones({
1609
+ pendingOps = [],
1610
+ syncMode = RECORD_TIMELABEL_SYNC_MODES.FULL,
1611
+ clientId = 'recordtimelabel-client',
1612
+ now = Date.now(),
1613
+ groupOrderNormalizer
1614
+ } = {}) => {
1615
+ const operationTime = toFiniteTimestamp(data?.lastModified) || now;
1616
+ const payloadSource = syncMode === RECORD_TIMELABEL_SYNC_MODES.MERGE
1617
+ ? mergeLocalRemote({
1618
+ localState: buildSyncStateInput(data || {}, operationTime, { groupOrderNormalizer }),
1619
+ remoteState: previousCloudData || {},
1620
+ pendingOps,
1621
+ clientId,
1622
+ now
1623
+ }).state
1624
+ : buildSyncStateInput(data || {}, operationTime, { groupOrderNormalizer });
1625
+ const sanitizedRecords = removeRecordSyncFlags(payloadSource.records || {});
1626
+ const deletionTombstones = buildDeletionTombstones({
1538
1627
  previousState: previousCloudData,
1539
1628
  nextRecords: sanitizedRecords,
1540
1629
  nextFolders: payloadSource.folders || [],
@@ -1544,11 +1633,12 @@ export const buildRecordTimeLabelSyncPayload = ({
1544
1633
  clientId
1545
1634
  });
1546
1635
 
1547
- return normalizeState({
1548
- ...payloadSource,
1549
- records: sanitizedRecords,
1550
- deletedRecordTombstones: deletionTombstones.deletedRecordTombstones,
1551
- deletedFolderTombstones: deletionTombstones.deletedFolderTombstones,
1636
+ return normalizeState({
1637
+ ...payloadSource,
1638
+ groupOrder: normalizeGroupOrderForOption(payloadSource.groupOrder, groupOrderNormalizer),
1639
+ records: sanitizedRecords,
1640
+ deletedRecordTombstones: deletionTombstones.deletedRecordTombstones,
1641
+ deletedFolderTombstones: deletionTombstones.deletedFolderTombstones,
1552
1642
  lastModified: operationTime
1553
1643
  });
1554
1644
  };
@@ -1569,7 +1659,8 @@ export const buildSyncPayloadWithOperations = ({
1569
1659
  pendingOps: [],
1570
1660
  syncMode,
1571
1661
  clientId,
1572
- now
1662
+ now,
1663
+ groupOrderNormalizer
1573
1664
  });
1574
1665
 
1575
1666
  operations.forEach((operation) => {
@@ -2122,6 +2213,7 @@ export default {
2122
2213
  buildFirestoreV1UserPatch,
2123
2214
  buildFirestoreV2LogicalPaths,
2124
2215
  buildFirestoreV2DocumentsFromState,
2216
+ buildFirestoreV2DocumentChangeSet,
2125
2217
  buildStateFromFirestoreV2Documents,
2126
2218
  flushPendingOperations,
2127
2219
  mergeRemoteStateIntoLocal,