@recordtimelabel/core 0.1.4 → 0.1.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +137 -25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recordtimelabel/core",
3
- "version": "0.1.4",
3
+ "version": "0.1.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
@@ -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.2';
8
+ export const RECORD_TIMELABEL_CORE_VERSION = '0.1.6';
9
9
 
10
10
  export const OPERATION_TYPES = Object.freeze({
11
11
  RECORD_CREATE: 'record.create',
@@ -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,57 @@ 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
+ nextSyncMeta.lastOrderClientIdByType = {
566
+ ...(previousSyncMeta?.lastOrderClientIdByType || {}),
567
+ [operation.type]: operation.clientId || previousSyncMeta?.lastClientId || null
568
+ };
569
+ }
570
+
571
+ return nextSyncMeta;
572
+ };
573
+
574
+ export const applyOperation = (state = {}, operation = {}) => {
526
575
  const normalized = normalizeState(state);
527
576
  const type = operation.type;
528
577
  const payload = operation.payload || {};
@@ -723,16 +772,15 @@ export const applyOperation = (state = {}, operation = {}) => {
723
772
 
724
773
  default:
725
774
  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
- };
775
+ }
776
+
777
+ nextState.lastModified = Math.max(nextState.lastModified || 0, operationTime);
778
+ nextState.rtlSyncMeta = buildOperationSyncMeta(
779
+ nextState.rtlSyncMeta,
780
+ operation,
781
+ payload,
782
+ operationTime
783
+ );
736
784
 
737
785
  return normalizeState(nextState);
738
786
  };
@@ -1334,6 +1382,12 @@ const getLastLocalOperationType = (data = {}) => String(
1334
1382
  ''
1335
1383
  );
1336
1384
 
1385
+ const getLastRemoteOperationType = (data = {}) => String(
1386
+ data?.rtlSyncMeta?.lastOperationType ||
1387
+ data?.syncMeta?.lastRemoteOperationType ||
1388
+ ''
1389
+ );
1390
+
1337
1391
  const getLastLocalOperationAt = (data = {}) => toFiniteTimestamp(
1338
1392
  data?.rtlSyncMeta?.lastOperationAt ||
1339
1393
  data?.rtlSyncMeta?.lastLocalOperationAt ||
@@ -1347,23 +1401,81 @@ const getLastRemoteOperationAt = (data = {}) => toFiniteTimestamp(
1347
1401
  data?.lastModified
1348
1402
  );
1349
1403
 
1404
+ const getLastClientId = (data = {}) => String(
1405
+ data?.rtlSyncMeta?.lastClientId ||
1406
+ data?.syncMeta?.lastClientId ||
1407
+ data?.syncMeta?.lastMergedClientId ||
1408
+ ''
1409
+ );
1410
+
1411
+ const getOrderOperationAtByType = (data = {}, operationType) => toFiniteTimestamp(
1412
+ data?.rtlSyncMeta?.lastOrderOperationAtByType?.[operationType] ||
1413
+ data?.syncMeta?.lastOrderOperationAtByType?.[operationType]
1414
+ );
1415
+
1416
+ const getOrderClientIdByType = (data = {}, operationType) => String(
1417
+ data?.rtlSyncMeta?.lastOrderClientIdByType?.[operationType] ||
1418
+ data?.syncMeta?.lastOrderClientIdByType?.[operationType] ||
1419
+ ''
1420
+ );
1421
+
1422
+ const hasOperationMarker = (data = {}, operationType, operationTypeResolver = getLastRemoteOperationType) => {
1423
+ const lastOperationId = String(data?.rtlSyncMeta?.lastOperationId || '');
1424
+ const lastOperationType = operationTypeResolver(data);
1425
+ return lastOperationId.startsWith(`${operationType}:`) || lastOperationType === operationType;
1426
+ };
1427
+
1428
+ const getMarkedOrderOperationAt = (data = {}, operationType, {
1429
+ operationTypeResolver = getLastRemoteOperationType,
1430
+ operationAtResolver = getLastRemoteOperationAt
1431
+ } = {}) => {
1432
+ const operationAtByType = getOrderOperationAtByType(data, operationType);
1433
+ if (operationAtByType) return operationAtByType;
1434
+ return hasOperationMarker(data, operationType, operationTypeResolver)
1435
+ ? operationAtResolver(data)
1436
+ : 0;
1437
+ };
1438
+
1350
1439
  const hasRecentLocalOperation = (localData = {}, remoteData = {}, operationType) => {
1351
- const lastOperationId = String(localData?.rtlSyncMeta?.lastOperationId || '');
1352
- const lastOperationType = getLastLocalOperationType(localData);
1353
- if (!lastOperationId.startsWith(`${operationType}:`) && lastOperationType !== operationType) {
1440
+ const localOperationAt = getMarkedOrderOperationAt(localData, operationType, {
1441
+ operationTypeResolver: getLastLocalOperationType,
1442
+ operationAtResolver: getLastLocalOperationAt
1443
+ });
1444
+ if (!localOperationAt) {
1354
1445
  return false;
1355
1446
  }
1356
1447
 
1357
- const localOperationAt = getLastLocalOperationAt(localData);
1358
- const remoteOperationAt = getLastRemoteOperationAt(remoteData);
1448
+ const remoteOrderOperationAt = getMarkedOrderOperationAt(remoteData, operationType, {
1449
+ operationTypeResolver: getLastRemoteOperationType,
1450
+ operationAtResolver: getLastRemoteOperationAt
1451
+ });
1452
+ const remoteOperationAt = remoteOrderOperationAt || getLastRemoteOperationAt(remoteData);
1359
1453
  return !localOperationAt || !remoteOperationAt || localOperationAt >= remoteOperationAt;
1360
1454
  };
1361
1455
 
1456
+ const hasRemoteOrderOperationFromDifferentClient = (localData = {}, remoteData = {}, operationType) => {
1457
+ const remoteOrderOperationAt = getMarkedOrderOperationAt(remoteData, operationType, {
1458
+ operationTypeResolver: getLastRemoteOperationType,
1459
+ operationAtResolver: getLastRemoteOperationAt
1460
+ });
1461
+ if (!remoteOrderOperationAt) {
1462
+ return false;
1463
+ }
1464
+
1465
+ const remoteClientId = getOrderClientIdByType(remoteData, operationType) || getLastClientId(remoteData);
1466
+ const localClientId = getLastClientId(localData);
1467
+ return Boolean(remoteClientId) && (!localClientId || remoteClientId !== localClientId);
1468
+ };
1469
+
1362
1470
  const shouldPreferRecentLocalOrder = (localData = {}, remoteData = {}, operationType) => {
1363
1471
  if (hasRecentLocalOperation(localData, remoteData, operationType)) {
1364
1472
  return true;
1365
1473
  }
1366
1474
 
1475
+ if (hasRemoteOrderOperationFromDifferentClient(localData, remoteData, operationType)) {
1476
+ return false;
1477
+ }
1478
+
1367
1479
  const localOperationAt = getLastLocalOperationAt(localData);
1368
1480
  const remoteOperationAt = getLastRemoteOperationAt(remoteData);
1369
1481
  return Boolean(localOperationAt) && (!remoteOperationAt || localOperationAt >= remoteOperationAt);