@recordtimelabel/core 0.1.3 → 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.
- package/package.json +1 -1
- package/src/index.js +279 -28
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
};
|
|
@@ -989,6 +1033,146 @@ const mapValuesArray = (value = {}) => {
|
|
|
989
1033
|
return Object.values(value);
|
|
990
1034
|
};
|
|
991
1035
|
|
|
1036
|
+
const buildLiveV2RecordIdsByFolder = (recordDocuments = {}) => {
|
|
1037
|
+
const liveRecordIdsByFolder = {};
|
|
1038
|
+
const liveRecordIdSetsByFolder = {};
|
|
1039
|
+
|
|
1040
|
+
Object.values(recordDocuments || {}).forEach((recordDoc) => {
|
|
1041
|
+
const recordId = normalizeId(recordDoc?.id);
|
|
1042
|
+
const folderId = safeFolderId(recordDoc?.folderId);
|
|
1043
|
+
if (!recordId) return;
|
|
1044
|
+
|
|
1045
|
+
if (!liveRecordIdsByFolder[folderId]) {
|
|
1046
|
+
liveRecordIdsByFolder[folderId] = [];
|
|
1047
|
+
liveRecordIdSetsByFolder[folderId] = new Set();
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (liveRecordIdSetsByFolder[folderId].has(recordId)) return;
|
|
1051
|
+
liveRecordIdSetsByFolder[folderId].add(recordId);
|
|
1052
|
+
liveRecordIdsByFolder[folderId].push(recordId);
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
return {
|
|
1056
|
+
liveRecordIdsByFolder,
|
|
1057
|
+
liveRecordIdSetsByFolder
|
|
1058
|
+
};
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
const buildV2RecordOrderByFolder = (records = {}, recordDocuments = {}) => {
|
|
1062
|
+
const {
|
|
1063
|
+
liveRecordIdsByFolder,
|
|
1064
|
+
liveRecordIdSetsByFolder
|
|
1065
|
+
} = buildLiveV2RecordIdsByFolder(recordDocuments);
|
|
1066
|
+
const hasLiveRecordDocuments = Object.keys(recordDocuments || {}).length > 0;
|
|
1067
|
+
const orders = {};
|
|
1068
|
+
const seenByFolder = {};
|
|
1069
|
+
|
|
1070
|
+
Object.keys(liveRecordIdsByFolder).forEach((folderId) => {
|
|
1071
|
+
orders[folderId] = [];
|
|
1072
|
+
seenByFolder[folderId] = new Set();
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
Object.entries(records || {}).forEach(([folderId, folderRecords]) => {
|
|
1076
|
+
const normalizedFolderId = safeFolderId(folderId);
|
|
1077
|
+
if (hasLiveRecordDocuments && !liveRecordIdSetsByFolder[normalizedFolderId]) return;
|
|
1078
|
+
|
|
1079
|
+
const seen = seenByFolder[normalizedFolderId] || new Set();
|
|
1080
|
+
orders[normalizedFolderId] = orders[normalizedFolderId] || [];
|
|
1081
|
+
|
|
1082
|
+
toArray(folderRecords).forEach((record) => {
|
|
1083
|
+
const recordId = normalizeId(record?.id);
|
|
1084
|
+
if (!recordId || seen.has(recordId)) return;
|
|
1085
|
+
if (
|
|
1086
|
+
liveRecordIdSetsByFolder[normalizedFolderId] &&
|
|
1087
|
+
!liveRecordIdSetsByFolder[normalizedFolderId].has(recordId)
|
|
1088
|
+
) {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
seen.add(recordId);
|
|
1093
|
+
orders[normalizedFolderId].push(recordId);
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
seenByFolder[normalizedFolderId] = seen;
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
Object.entries(liveRecordIdsByFolder).forEach(([folderId, liveRecordIds]) => {
|
|
1100
|
+
const seen = seenByFolder[folderId] || new Set();
|
|
1101
|
+
orders[folderId] = orders[folderId] || [];
|
|
1102
|
+
|
|
1103
|
+
liveRecordIds.forEach((recordId) => {
|
|
1104
|
+
if (seen.has(recordId)) return;
|
|
1105
|
+
seen.add(recordId);
|
|
1106
|
+
orders[folderId].push(recordId);
|
|
1107
|
+
});
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
return orders;
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
const extractV2RecordOrderByFolder = (folderDocuments = {}) => {
|
|
1114
|
+
const orders = {};
|
|
1115
|
+
|
|
1116
|
+
Object.entries(folderDocuments || {}).forEach(([rawFolderId, folderDoc]) => {
|
|
1117
|
+
const folderId = normalizeId(folderDoc?.id || rawFolderId);
|
|
1118
|
+
if (!folderId || !Array.isArray(folderDoc?.recordOrder)) return;
|
|
1119
|
+
|
|
1120
|
+
const seen = new Set();
|
|
1121
|
+
orders[folderId] = [];
|
|
1122
|
+
|
|
1123
|
+
folderDoc.recordOrder.forEach((recordId) => {
|
|
1124
|
+
const normalized = normalizeId(recordId);
|
|
1125
|
+
if (!normalized || seen.has(normalized)) return;
|
|
1126
|
+
seen.add(normalized);
|
|
1127
|
+
orders[folderId].push(normalized);
|
|
1128
|
+
});
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
return orders;
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
const reorderV2RecordsByFolderRecordOrder = (records = {}, recordOrderByFolder = {}) => {
|
|
1135
|
+
const nextRecords = {};
|
|
1136
|
+
|
|
1137
|
+
Object.entries(records || {}).forEach(([folderId, folderRecords]) => {
|
|
1138
|
+
const fallbackRecords = toArray(folderRecords);
|
|
1139
|
+
const recordOrder = recordOrderByFolder[folderId];
|
|
1140
|
+
|
|
1141
|
+
if (!Array.isArray(recordOrder)) {
|
|
1142
|
+
nextRecords[folderId] = fallbackRecords;
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const recordsById = new Map();
|
|
1147
|
+
fallbackRecords.forEach((record) => {
|
|
1148
|
+
const recordId = normalizeId(record?.id);
|
|
1149
|
+
if (recordId && !recordsById.has(recordId)) {
|
|
1150
|
+
recordsById.set(recordId, { ...record, id: recordId });
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
const seen = new Set();
|
|
1155
|
+
const orderedRecords = [];
|
|
1156
|
+
recordOrder.forEach((recordId) => {
|
|
1157
|
+
const record = recordsById.get(recordId);
|
|
1158
|
+
if (!record || seen.has(recordId)) return;
|
|
1159
|
+
seen.add(recordId);
|
|
1160
|
+
orderedRecords.push(record);
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
fallbackRecords.forEach((record) => {
|
|
1164
|
+
const recordId = normalizeId(record?.id);
|
|
1165
|
+
if (!recordId || seen.has(recordId)) return;
|
|
1166
|
+
seen.add(recordId);
|
|
1167
|
+
orderedRecords.push(record);
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
nextRecords[folderId] = orderedRecords;
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
return nextRecords;
|
|
1174
|
+
};
|
|
1175
|
+
|
|
992
1176
|
const cleanV2RecordDocument = (record = {}, folderId, now) => {
|
|
993
1177
|
const recordId = normalizeId(record.id);
|
|
994
1178
|
if (!recordId) return null;
|
|
@@ -1005,8 +1189,15 @@ const cleanV2RecordDocument = (record = {}, folderId, now) => {
|
|
|
1005
1189
|
const cleanV2FolderDocument = (folder = {}, now) => {
|
|
1006
1190
|
const folderId = normalizeId(folder.id);
|
|
1007
1191
|
if (!folderId) return null;
|
|
1192
|
+
const {
|
|
1193
|
+
schemaVersion,
|
|
1194
|
+
recordOrder,
|
|
1195
|
+
groupOrder,
|
|
1196
|
+
folderOrder,
|
|
1197
|
+
...folderFields
|
|
1198
|
+
} = folder || {};
|
|
1008
1199
|
return {
|
|
1009
|
-
...
|
|
1200
|
+
...folderFields,
|
|
1010
1201
|
id: folderId,
|
|
1011
1202
|
updatedAt: folder.updatedAt || folder.createdAt || now,
|
|
1012
1203
|
schemaVersion: 2
|
|
@@ -1035,10 +1226,16 @@ export const buildFirestoreV2DocumentsFromState = (state = {}, options = {}) =>
|
|
|
1035
1226
|
});
|
|
1036
1227
|
});
|
|
1037
1228
|
|
|
1229
|
+
const recordOrderByFolder = buildV2RecordOrderByFolder(normalized.records || {}, records);
|
|
1038
1230
|
const folders = {};
|
|
1039
1231
|
toArray(normalized.folders).forEach((folder) => {
|
|
1040
1232
|
const folderDoc = cleanV2FolderDocument(folder, now);
|
|
1041
|
-
if (folderDoc)
|
|
1233
|
+
if (folderDoc) {
|
|
1234
|
+
folders[folderDoc.id] = {
|
|
1235
|
+
...folderDoc,
|
|
1236
|
+
recordOrder: recordOrderByFolder[folderDoc.id] || []
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1042
1239
|
});
|
|
1043
1240
|
|
|
1044
1241
|
const ops = {};
|
|
@@ -1094,11 +1291,12 @@ export const buildStateFromFirestoreV2Documents = (documents = {}, options = {})
|
|
|
1094
1291
|
];
|
|
1095
1292
|
});
|
|
1096
1293
|
|
|
1294
|
+
const recordOrderByFolder = extractV2RecordOrderByFolder(documents?.folders);
|
|
1097
1295
|
const folders = mapValuesArray(documents?.folders)
|
|
1098
1296
|
.map((folderDoc) => {
|
|
1099
1297
|
const folderId = normalizeId(folderDoc?.id);
|
|
1100
1298
|
if (!folderId) return null;
|
|
1101
|
-
const { schemaVersion, ...folder } = folderDoc;
|
|
1299
|
+
const { schemaVersion, recordOrder, groupOrder, folderOrder, ...folder } = folderDoc;
|
|
1102
1300
|
return { ...folder, id: folderId };
|
|
1103
1301
|
})
|
|
1104
1302
|
.filter(Boolean);
|
|
@@ -1114,7 +1312,7 @@ export const buildStateFromFirestoreV2Documents = (documents = {}, options = {})
|
|
|
1114
1312
|
|
|
1115
1313
|
const state = normalizeState({
|
|
1116
1314
|
folders,
|
|
1117
|
-
records,
|
|
1315
|
+
records: reorderV2RecordsByFolderRecordOrder(records, recordOrderByFolder),
|
|
1118
1316
|
settings: rootDoc.settings || {},
|
|
1119
1317
|
groupOrder: normalizeGroupOrderForOption(rootDoc.groupOrder, options.groupOrderNormalizer),
|
|
1120
1318
|
folderOrder: rootDoc.folderOrder || [],
|
|
@@ -1180,6 +1378,12 @@ const getLastLocalOperationType = (data = {}) => String(
|
|
|
1180
1378
|
''
|
|
1181
1379
|
);
|
|
1182
1380
|
|
|
1381
|
+
const getLastRemoteOperationType = (data = {}) => String(
|
|
1382
|
+
data?.rtlSyncMeta?.lastOperationType ||
|
|
1383
|
+
data?.syncMeta?.lastRemoteOperationType ||
|
|
1384
|
+
''
|
|
1385
|
+
);
|
|
1386
|
+
|
|
1183
1387
|
const getLastLocalOperationAt = (data = {}) => toFiniteTimestamp(
|
|
1184
1388
|
data?.rtlSyncMeta?.lastOperationAt ||
|
|
1185
1389
|
data?.rtlSyncMeta?.lastLocalOperationAt ||
|
|
@@ -1193,23 +1397,70 @@ const getLastRemoteOperationAt = (data = {}) => toFiniteTimestamp(
|
|
|
1193
1397
|
data?.lastModified
|
|
1194
1398
|
);
|
|
1195
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
|
+
|
|
1196
1429
|
const hasRecentLocalOperation = (localData = {}, remoteData = {}, operationType) => {
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
|
|
1430
|
+
const localOperationAt = getMarkedOrderOperationAt(localData, operationType, {
|
|
1431
|
+
operationTypeResolver: getLastLocalOperationType,
|
|
1432
|
+
operationAtResolver: getLastLocalOperationAt
|
|
1433
|
+
});
|
|
1434
|
+
if (!localOperationAt) {
|
|
1200
1435
|
return false;
|
|
1201
1436
|
}
|
|
1202
1437
|
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1438
|
+
const remoteOrderOperationAt = getMarkedOrderOperationAt(remoteData, operationType, {
|
|
1439
|
+
operationTypeResolver: getLastRemoteOperationType,
|
|
1440
|
+
operationAtResolver: getLastRemoteOperationAt
|
|
1441
|
+
});
|
|
1442
|
+
const remoteOperationAt = remoteOrderOperationAt || getLastRemoteOperationAt(remoteData);
|
|
1205
1443
|
return !localOperationAt || !remoteOperationAt || localOperationAt >= remoteOperationAt;
|
|
1206
1444
|
};
|
|
1207
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
|
+
|
|
1208
1455
|
const shouldPreferRecentLocalOrder = (localData = {}, remoteData = {}, operationType) => {
|
|
1209
1456
|
if (hasRecentLocalOperation(localData, remoteData, operationType)) {
|
|
1210
1457
|
return true;
|
|
1211
1458
|
}
|
|
1212
1459
|
|
|
1460
|
+
if (hasRemoteOrderOperationFromDifferentClient(localData, remoteData, operationType)) {
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1213
1464
|
const localOperationAt = getLastLocalOperationAt(localData);
|
|
1214
1465
|
const remoteOperationAt = getLastRemoteOperationAt(remoteData);
|
|
1215
1466
|
return Boolean(localOperationAt) && (!remoteOperationAt || localOperationAt >= remoteOperationAt);
|