@recordtimelabel/core 0.1.4 → 0.1.5

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +121 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recordtimelabel/core",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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
@@ -21,6 +21,13 @@ export const OPERATION_TYPES = Object.freeze({
21
21
  EXPANDED_GROUPS_UPDATE: 'expandedGroups.update',
22
22
  SETTINGS_UPDATE: 'settings.update'
23
23
  });
24
+
25
+ const ORDER_OPERATION_TYPES = new Set([
26
+ OPERATION_TYPES.RECORD_REORDER,
27
+ OPERATION_TYPES.FOLDER_REORDER,
28
+ OPERATION_TYPES.GROUP_REORDER,
29
+ OPERATION_TYPES.EXPANDED_GROUPS_UPDATE
30
+ ]);
24
31
 
25
32
  export const RECORD_TIMELABEL_SYNC_MODES = Object.freeze({
26
33
  FULL: 'full',
@@ -514,15 +521,53 @@ export const preserveRecordOrderFromLocal = (records = {}, localRecords = {}) =>
514
521
  return result;
515
522
  };
516
523
 
517
- export const createOperation = (type, payload = {}, options = {}) => ({
518
- id: options.id || `${type}:${options.clientId || 'client'}:${options.now || Date.now()}:${Math.random().toString(36).slice(2, 10)}`,
519
- type,
520
- payload,
521
- clientId: options.clientId || null,
522
- createdAt: options.now || Date.now()
523
- });
524
-
525
- export const applyOperation = (state = {}, operation = {}) => {
524
+ export const createOperation = (type, payload = {}, options = {}) => ({
525
+ id: options.id || `${type}:${options.clientId || 'client'}:${options.now || Date.now()}:${Math.random().toString(36).slice(2, 10)}`,
526
+ type,
527
+ payload,
528
+ clientId: options.clientId || null,
529
+ createdAt: options.now || Date.now()
530
+ });
531
+
532
+ const getOperationClientInstanceId = (operation = {}, payload = {}) => (
533
+ operation.clientInstanceId ||
534
+ operation.instanceId ||
535
+ payload.clientInstanceId ||
536
+ payload.instanceId ||
537
+ null
538
+ );
539
+
540
+ const buildOperationSyncMeta = (previousSyncMeta = {}, operation = {}, payload = {}, operationTime) => {
541
+ const nextSyncMeta = {
542
+ ...(previousSyncMeta || {}),
543
+ lastOperationId: operation.id || previousSyncMeta?.lastOperationId || null,
544
+ lastOperationType: operation.type || previousSyncMeta?.lastOperationType || null,
545
+ lastClientId: operation.clientId || previousSyncMeta?.lastClientId || null,
546
+ lastOperationAt: operationTime
547
+ };
548
+ const nextClientInstanceId = getOperationClientInstanceId(operation, payload);
549
+
550
+ if (nextClientInstanceId) {
551
+ nextSyncMeta.lastClientInstanceId = nextClientInstanceId;
552
+ } else if (operation.clientId) {
553
+ delete nextSyncMeta.lastClientInstanceId;
554
+ }
555
+
556
+ if (ORDER_OPERATION_TYPES.has(operation.type)) {
557
+ nextSyncMeta.lastOrderOperationAtByType = {
558
+ ...(previousSyncMeta?.lastOrderOperationAtByType || {}),
559
+ [operation.type]: operationTime
560
+ };
561
+ nextSyncMeta.lastOrderOperationIdByType = {
562
+ ...(previousSyncMeta?.lastOrderOperationIdByType || {}),
563
+ [operation.type]: operation.id || null
564
+ };
565
+ }
566
+
567
+ return nextSyncMeta;
568
+ };
569
+
570
+ export const applyOperation = (state = {}, operation = {}) => {
526
571
  const normalized = normalizeState(state);
527
572
  const type = operation.type;
528
573
  const payload = operation.payload || {};
@@ -723,16 +768,15 @@ export const applyOperation = (state = {}, operation = {}) => {
723
768
 
724
769
  default:
725
770
  return normalized;
726
- }
727
-
728
- nextState.lastModified = Math.max(nextState.lastModified || 0, operationTime);
729
- nextState.rtlSyncMeta = {
730
- ...nextState.rtlSyncMeta,
731
- lastOperationId: operation.id || nextState.rtlSyncMeta.lastOperationId || null,
732
- lastOperationType: operation.type || nextState.rtlSyncMeta.lastOperationType || null,
733
- lastClientId: operation.clientId || nextState.rtlSyncMeta.lastClientId || null,
734
- lastOperationAt: operationTime
735
- };
771
+ }
772
+
773
+ nextState.lastModified = Math.max(nextState.lastModified || 0, operationTime);
774
+ nextState.rtlSyncMeta = buildOperationSyncMeta(
775
+ nextState.rtlSyncMeta,
776
+ operation,
777
+ payload,
778
+ operationTime
779
+ );
736
780
 
737
781
  return normalizeState(nextState);
738
782
  };
@@ -1334,6 +1378,12 @@ const getLastLocalOperationType = (data = {}) => String(
1334
1378
  ''
1335
1379
  );
1336
1380
 
1381
+ const getLastRemoteOperationType = (data = {}) => String(
1382
+ data?.rtlSyncMeta?.lastOperationType ||
1383
+ data?.syncMeta?.lastRemoteOperationType ||
1384
+ ''
1385
+ );
1386
+
1337
1387
  const getLastLocalOperationAt = (data = {}) => toFiniteTimestamp(
1338
1388
  data?.rtlSyncMeta?.lastOperationAt ||
1339
1389
  data?.rtlSyncMeta?.lastLocalOperationAt ||
@@ -1347,23 +1397,70 @@ const getLastRemoteOperationAt = (data = {}) => toFiniteTimestamp(
1347
1397
  data?.lastModified
1348
1398
  );
1349
1399
 
1400
+ const getLastClientId = (data = {}) => String(
1401
+ data?.rtlSyncMeta?.lastClientId ||
1402
+ data?.syncMeta?.lastClientId ||
1403
+ data?.syncMeta?.lastMergedClientId ||
1404
+ ''
1405
+ );
1406
+
1407
+ const getOrderOperationAtByType = (data = {}, operationType) => toFiniteTimestamp(
1408
+ data?.rtlSyncMeta?.lastOrderOperationAtByType?.[operationType] ||
1409
+ data?.syncMeta?.lastOrderOperationAtByType?.[operationType]
1410
+ );
1411
+
1412
+ const hasOperationMarker = (data = {}, operationType, operationTypeResolver = getLastRemoteOperationType) => {
1413
+ const lastOperationId = String(data?.rtlSyncMeta?.lastOperationId || '');
1414
+ const lastOperationType = operationTypeResolver(data);
1415
+ return lastOperationId.startsWith(`${operationType}:`) || lastOperationType === operationType;
1416
+ };
1417
+
1418
+ const getMarkedOrderOperationAt = (data = {}, operationType, {
1419
+ operationTypeResolver = getLastRemoteOperationType,
1420
+ operationAtResolver = getLastRemoteOperationAt
1421
+ } = {}) => {
1422
+ const operationAtByType = getOrderOperationAtByType(data, operationType);
1423
+ if (operationAtByType) return operationAtByType;
1424
+ return hasOperationMarker(data, operationType, operationTypeResolver)
1425
+ ? operationAtResolver(data)
1426
+ : 0;
1427
+ };
1428
+
1350
1429
  const hasRecentLocalOperation = (localData = {}, remoteData = {}, operationType) => {
1351
- const lastOperationId = String(localData?.rtlSyncMeta?.lastOperationId || '');
1352
- const lastOperationType = getLastLocalOperationType(localData);
1353
- if (!lastOperationId.startsWith(`${operationType}:`) && lastOperationType !== operationType) {
1430
+ const localOperationAt = getMarkedOrderOperationAt(localData, operationType, {
1431
+ operationTypeResolver: getLastLocalOperationType,
1432
+ operationAtResolver: getLastLocalOperationAt
1433
+ });
1434
+ if (!localOperationAt) {
1354
1435
  return false;
1355
1436
  }
1356
1437
 
1357
- const localOperationAt = getLastLocalOperationAt(localData);
1358
- const remoteOperationAt = getLastRemoteOperationAt(remoteData);
1438
+ const remoteOrderOperationAt = getMarkedOrderOperationAt(remoteData, operationType, {
1439
+ operationTypeResolver: getLastRemoteOperationType,
1440
+ operationAtResolver: getLastRemoteOperationAt
1441
+ });
1442
+ const remoteOperationAt = remoteOrderOperationAt || getLastRemoteOperationAt(remoteData);
1359
1443
  return !localOperationAt || !remoteOperationAt || localOperationAt >= remoteOperationAt;
1360
1444
  };
1361
1445
 
1446
+ const hasRemoteOrderOperationFromDifferentClient = (localData = {}, remoteData = {}, operationType) => {
1447
+ if (!hasOperationMarker(remoteData, operationType, getLastRemoteOperationType)) {
1448
+ return false;
1449
+ }
1450
+
1451
+ const remoteClientId = getLastClientId(remoteData);
1452
+ return Boolean(remoteClientId) && remoteClientId !== getLastClientId(localData);
1453
+ };
1454
+
1362
1455
  const shouldPreferRecentLocalOrder = (localData = {}, remoteData = {}, operationType) => {
1363
1456
  if (hasRecentLocalOperation(localData, remoteData, operationType)) {
1364
1457
  return true;
1365
1458
  }
1366
1459
 
1460
+ if (hasRemoteOrderOperationFromDifferentClient(localData, remoteData, operationType)) {
1461
+ return false;
1462
+ }
1463
+
1367
1464
  const localOperationAt = getLastLocalOperationAt(localData);
1368
1465
  const remoteOperationAt = getLastRemoteOperationAt(remoteData);
1369
1466
  return Boolean(localOperationAt) && (!remoteOperationAt || localOperationAt >= remoteOperationAt);