@recordtimelabel/core 0.6.0 → 0.6.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 +2 -2
- package/package.json +1 -1
- package/src/changefeed.js +41 -5
- package/src/domain/group-identity.js +38 -1
- package/src/domain/hash.js +6 -0
- package/src/domain/twitch-vod.js +20 -1
- package/src/domain.js +6 -0
- package/src/index.js +159 -43
package/README.md
CHANGED
|
@@ -17,10 +17,10 @@ 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 published artifact is `0.6.
|
|
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 artifact is `0.6.1`:
|
|
21
21
|
|
|
22
22
|
```json
|
|
23
|
-
"@recordtimelabel/core": "0.6.
|
|
23
|
+
"@recordtimelabel/core": "0.6.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.
|
package/package.json
CHANGED
package/src/changefeed.js
CHANGED
|
@@ -11,6 +11,27 @@ export const FIRESTORE_V2_BOOTSTRAP_REASONS = Object.freeze({
|
|
|
11
11
|
const toId = (value) => String(value || '').trim();
|
|
12
12
|
const toIdList = (value) => (Array.isArray(value) ? value : []).map(toId).filter(Boolean);
|
|
13
13
|
|
|
14
|
+
const decodeLifecycleTombstoneId = (value) => {
|
|
15
|
+
const id = toId(value);
|
|
16
|
+
if (!id) return '';
|
|
17
|
+
try {
|
|
18
|
+
return decodeURIComponent(id);
|
|
19
|
+
} catch {
|
|
20
|
+
return id;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const lifecycleTombstoneDocumentKey = (value) => {
|
|
25
|
+
const logicalId = decodeLifecycleTombstoneId(value);
|
|
26
|
+
return logicalId ? encodeURIComponent(logicalId) : '';
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const lifecycleTombstoneKeyAliases = (value) => {
|
|
30
|
+
const logicalId = decodeLifecycleTombstoneId(value);
|
|
31
|
+
if (!logicalId) return [];
|
|
32
|
+
return [...new Set([toId(value), logicalId, encodeURIComponent(logicalId)].filter(Boolean))];
|
|
33
|
+
};
|
|
34
|
+
|
|
14
35
|
const cloneDocuments = (documents = {}) => ({
|
|
15
36
|
root: documents.root ? {...documents.root} : documents.root,
|
|
16
37
|
records: {...(documents.records || {})},
|
|
@@ -113,13 +134,18 @@ export const applyFirestoreV2ResolvedChangeBatch = ({
|
|
|
113
134
|
? change.lifecycleTombstoneUpserts
|
|
114
135
|
: [];
|
|
115
136
|
const lifecycleDeletes = toIdList(change.deletedLifecycleTombstoneIds);
|
|
116
|
-
const lifecycleUpsertIds = lifecycleUpserts
|
|
137
|
+
const lifecycleUpsertIds = lifecycleUpserts
|
|
138
|
+
.map((entry) => lifecycleTombstoneDocumentKey(entry?.id))
|
|
139
|
+
.filter(Boolean);
|
|
140
|
+
const lifecycleDeleteIds = lifecycleDeletes
|
|
141
|
+
.map((id) => lifecycleTombstoneDocumentKey(id))
|
|
142
|
+
.filter(Boolean);
|
|
117
143
|
|
|
118
144
|
if (
|
|
119
145
|
hasConflict(changedRecords, deletedRecords) ||
|
|
120
146
|
hasConflict(changedFolders, deletedFolders) ||
|
|
121
147
|
hasConflict(changedTrash, deletedTrash) ||
|
|
122
|
-
hasConflict(lifecycleUpsertIds,
|
|
148
|
+
hasConflict(lifecycleUpsertIds, lifecycleDeleteIds)
|
|
123
149
|
) {
|
|
124
150
|
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.INVALID_CHANGE_CONFLICT, inputCache);
|
|
125
151
|
}
|
|
@@ -161,11 +187,21 @@ export const applyFirestoreV2ResolvedChangeBatch = ({
|
|
|
161
187
|
deletedFolders.forEach((id) => delete candidate.documents.folders[id]);
|
|
162
188
|
deletedTrash.forEach((id) => delete candidate.documents.trash[id]);
|
|
163
189
|
lifecycleUpserts.forEach((entry) => {
|
|
164
|
-
const
|
|
165
|
-
|
|
190
|
+
const logicalId = decodeLifecycleTombstoneId(entry?.id);
|
|
191
|
+
const documentKey = lifecycleTombstoneDocumentKey(logicalId);
|
|
192
|
+
if (!documentKey) return;
|
|
193
|
+
lifecycleTombstoneKeyAliases(logicalId).forEach((alias) => {
|
|
194
|
+
delete candidate.documents.lifecycleTombstones[alias];
|
|
195
|
+
});
|
|
196
|
+
candidate.documents.lifecycleTombstones[documentKey] = {
|
|
197
|
+
...entry,
|
|
198
|
+
id: logicalId
|
|
199
|
+
};
|
|
166
200
|
});
|
|
167
201
|
lifecycleDeletes.forEach((id) => {
|
|
168
|
-
|
|
202
|
+
lifecycleTombstoneKeyAliases(id).forEach((alias) => {
|
|
203
|
+
delete candidate.documents.lifecycleTombstones[alias];
|
|
204
|
+
});
|
|
169
205
|
});
|
|
170
206
|
candidate.revision = revision;
|
|
171
207
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createdAtForStableRecordTimeLabelOperationId,
|
|
3
|
+
fingerprintCanonicalJson
|
|
4
|
+
} from './hash.js';
|
|
2
5
|
import {
|
|
3
6
|
DEFAULT_UNKNOWN_CHANNEL,
|
|
4
7
|
DEFAULT_UNKNOWN_TITLE,
|
|
@@ -309,6 +312,26 @@ export const buildRecordTimeLabelGroupIndex = ({
|
|
|
309
312
|
);
|
|
310
313
|
addAliasTarget(aliasTargetsByGroupId, buildLegacyRecordTimeLabelGroupId(record, platform, unknownTitle), groupId);
|
|
311
314
|
addAliasTarget(aliasTargetsByGroupId, buildFallbackRecordGroupId(record, platform, unknownTitle), groupId);
|
|
315
|
+
const declaredPlatform = normalizeText(record?.platform).toLowerCase();
|
|
316
|
+
if (declaredPlatform !== 'twitch' && declaredPlatform !== 'youtube') {
|
|
317
|
+
['twitch', 'youtube'].forEach((themePlatform) => {
|
|
318
|
+
addAliasTarget(
|
|
319
|
+
aliasTargetsByGroupId,
|
|
320
|
+
buildLegacyRecordTimeLabelGroupId(record, themePlatform, unknownTitle),
|
|
321
|
+
groupId
|
|
322
|
+
);
|
|
323
|
+
addAliasTarget(
|
|
324
|
+
aliasTargetsByGroupId,
|
|
325
|
+
buildFallbackRecordGroupId(record, themePlatform, unknownTitle),
|
|
326
|
+
groupId
|
|
327
|
+
);
|
|
328
|
+
addAliasTarget(
|
|
329
|
+
aliasTargetsByGroupId,
|
|
330
|
+
buildRecordTimeLabelStandaloneGroupId(record, themePlatform, unknownTitle),
|
|
331
|
+
groupId
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
312
335
|
});
|
|
313
336
|
|
|
314
337
|
const groups = Object.values(groupsById).sort((left, right) => {
|
|
@@ -454,3 +477,17 @@ export const buildRecordTimeLabelGroupMetadata = ({
|
|
|
454
477
|
export const buildRecordTimeLabelGroupReorderOperationId = (previousOrder = [], nextOrder = []) => (
|
|
455
478
|
`group.reorder:canonical:${fingerprintCanonicalJson(normalizeIdList(previousOrder))}:${fingerprintCanonicalJson(normalizeIdList(nextOrder))}`
|
|
456
479
|
);
|
|
480
|
+
|
|
481
|
+
export const buildRecordTimeLabelGroupReorderOperation = ({
|
|
482
|
+
previousOrder = [],
|
|
483
|
+
nextOrder = []
|
|
484
|
+
} = {}) => {
|
|
485
|
+
const groupOrder = normalizeIdList(nextOrder);
|
|
486
|
+
const id = buildRecordTimeLabelGroupReorderOperationId(previousOrder, groupOrder);
|
|
487
|
+
return {
|
|
488
|
+
id,
|
|
489
|
+
type: 'group.reorder',
|
|
490
|
+
createdAt: createdAtForStableRecordTimeLabelOperationId(id),
|
|
491
|
+
payload: {groupOrder}
|
|
492
|
+
};
|
|
493
|
+
};
|
package/src/domain/hash.js
CHANGED
|
@@ -145,3 +145,9 @@ export const digestCanonicalJson = (value) => sha256Hex(canonicalizeJson(value))
|
|
|
145
145
|
export const fingerprintCanonicalJson = (value, length = 16) => (
|
|
146
146
|
digestCanonicalJson(value).slice(0, length)
|
|
147
147
|
);
|
|
148
|
+
|
|
149
|
+
export const createdAtForStableRecordTimeLabelOperationId = (operationId) => {
|
|
150
|
+
const digest = digestCanonicalJson(String(operationId || ''));
|
|
151
|
+
const value = Number.parseInt(digest.slice(0, 12), 16);
|
|
152
|
+
return Number.isSafeInteger(value) && value > 0 ? value : 1;
|
|
153
|
+
};
|
package/src/domain/twitch-vod.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createdAtForStableRecordTimeLabelOperationId,
|
|
3
|
+
fingerprintCanonicalJson
|
|
4
|
+
} from './hash.js';
|
|
2
5
|
import {buildRecordTimeLabelGroupIndex} from './group-identity.js';
|
|
3
6
|
import {
|
|
4
7
|
DEFAULT_UNKNOWN_TITLE,
|
|
@@ -197,6 +200,22 @@ export const buildRecordTimeLabelVodPatchOperationId = (recordId, patch = {}) =>
|
|
|
197
200
|
`record.update:${normalizeId(recordId)}:vod-patch:${fingerprintCanonicalJson(patch || {})}`
|
|
198
201
|
);
|
|
199
202
|
|
|
203
|
+
export const buildRecordTimeLabelVodPatchOperation = ({recordId, patch = {}} = {}) => {
|
|
204
|
+
const safePatch = {...(patch || {})};
|
|
205
|
+
delete safePatch.updatedAt;
|
|
206
|
+
const id = buildRecordTimeLabelVodPatchOperationId(recordId, safePatch);
|
|
207
|
+
const createdAt = createdAtForStableRecordTimeLabelOperationId(id);
|
|
208
|
+
return {
|
|
209
|
+
id,
|
|
210
|
+
type: 'record.update',
|
|
211
|
+
createdAt,
|
|
212
|
+
payload: {
|
|
213
|
+
recordId: normalizeId(recordId),
|
|
214
|
+
patch: {...safePatch, updatedAt: createdAt}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
|
|
200
219
|
export const buildKnownTwitchVodRecordPatches = ({
|
|
201
220
|
records = [],
|
|
202
221
|
groupIndex = null,
|
package/src/domain.js
CHANGED
|
@@ -10,11 +10,16 @@ export {
|
|
|
10
10
|
buildLegacyRecordTimeLabelGroupId,
|
|
11
11
|
buildRecordTimeLabelGroupIndex,
|
|
12
12
|
buildRecordTimeLabelGroupMetadata,
|
|
13
|
+
buildRecordTimeLabelGroupReorderOperation,
|
|
13
14
|
buildRecordTimeLabelGroupReorderOperationId,
|
|
14
15
|
buildRecordTimeLabelStandaloneGroupId,
|
|
15
16
|
canonicalizeRecordTimeLabelGroupView
|
|
16
17
|
} from './domain/group-identity.js';
|
|
17
18
|
|
|
19
|
+
export {
|
|
20
|
+
createdAtForStableRecordTimeLabelOperationId
|
|
21
|
+
} from './domain/hash.js';
|
|
22
|
+
|
|
18
23
|
export {
|
|
19
24
|
extractTwitchVodIdFromUrl,
|
|
20
25
|
extractYouTubeVideoIdFromUrl
|
|
@@ -22,6 +27,7 @@ export {
|
|
|
22
27
|
|
|
23
28
|
export {
|
|
24
29
|
buildKnownTwitchVodRecordPatches,
|
|
30
|
+
buildRecordTimeLabelVodPatchOperation,
|
|
25
31
|
buildRecordTimeLabelVodPatchOperationId,
|
|
26
32
|
calculateVodTitleSimilarity,
|
|
27
33
|
normalizeVodTitleForMatch,
|
package/src/index.js
CHANGED
|
@@ -37,7 +37,9 @@ import {
|
|
|
37
37
|
} from './snapshot-root.js';
|
|
38
38
|
import {
|
|
39
39
|
applyFirestoreV2ResolvedChangeBatch,
|
|
40
|
-
FIRESTORE_V2_BOOTSTRAP_REASONS
|
|
40
|
+
FIRESTORE_V2_BOOTSTRAP_REASONS,
|
|
41
|
+
lifecycleTombstoneDocumentKey,
|
|
42
|
+
lifecycleTombstoneKeyAliases
|
|
41
43
|
} from './changefeed.js';
|
|
42
44
|
|
|
43
45
|
export {
|
|
@@ -50,7 +52,9 @@ export {
|
|
|
50
52
|
} from './snapshot-root.js';
|
|
51
53
|
export {
|
|
52
54
|
applyFirestoreV2ResolvedChangeBatch,
|
|
53
|
-
FIRESTORE_V2_BOOTSTRAP_REASONS
|
|
55
|
+
FIRESTORE_V2_BOOTSTRAP_REASONS,
|
|
56
|
+
lifecycleTombstoneDocumentKey,
|
|
57
|
+
lifecycleTombstoneKeyAliases
|
|
54
58
|
} from './changefeed.js';
|
|
55
59
|
|
|
56
60
|
import {
|
|
@@ -61,11 +65,14 @@ import {
|
|
|
61
65
|
buildRecordTimeLabelChannelFolderPlan,
|
|
62
66
|
buildRecordTimeLabelGroupIndex,
|
|
63
67
|
buildRecordTimeLabelGroupMetadata,
|
|
68
|
+
buildRecordTimeLabelGroupReorderOperation,
|
|
64
69
|
buildRecordTimeLabelGroupReorderOperationId,
|
|
65
70
|
buildRecordTimeLabelStandaloneGroupId,
|
|
71
|
+
buildRecordTimeLabelVodPatchOperation,
|
|
66
72
|
buildRecordTimeLabelVodPatchOperationId,
|
|
67
73
|
calculateVodTitleSimilarity,
|
|
68
74
|
canonicalizeRecordTimeLabelGroupView,
|
|
75
|
+
createdAtForStableRecordTimeLabelOperationId,
|
|
69
76
|
collectRecordTimeLabelChannelCandidates,
|
|
70
77
|
extractChannelHandleFromUrl,
|
|
71
78
|
extractTwitchVodIdFromUrl,
|
|
@@ -89,11 +96,14 @@ export {
|
|
|
89
96
|
buildRecordTimeLabelChannelFolderPlan,
|
|
90
97
|
buildRecordTimeLabelGroupIndex,
|
|
91
98
|
buildRecordTimeLabelGroupMetadata,
|
|
99
|
+
buildRecordTimeLabelGroupReorderOperation,
|
|
92
100
|
buildRecordTimeLabelGroupReorderOperationId,
|
|
93
101
|
buildRecordTimeLabelStandaloneGroupId,
|
|
102
|
+
buildRecordTimeLabelVodPatchOperation,
|
|
94
103
|
buildRecordTimeLabelVodPatchOperationId,
|
|
95
104
|
calculateVodTitleSimilarity,
|
|
96
105
|
canonicalizeRecordTimeLabelGroupView,
|
|
106
|
+
createdAtForStableRecordTimeLabelOperationId,
|
|
97
107
|
collectRecordTimeLabelChannelCandidates,
|
|
98
108
|
extractChannelHandleFromUrl,
|
|
99
109
|
extractTwitchVodIdFromUrl,
|
|
@@ -109,7 +119,7 @@ export {
|
|
|
109
119
|
selectBestMatchingTwitchVod
|
|
110
120
|
};
|
|
111
121
|
|
|
112
|
-
export const RECORD_TIMELABEL_CORE_VERSION = '0.6.
|
|
122
|
+
export const RECORD_TIMELABEL_CORE_VERSION = '0.6.1';
|
|
113
123
|
export const RTL_SYNC_PROTOCOL_VERSION = 2;
|
|
114
124
|
export const RECORD_TIMELABEL_DURABLE_ENGINE_CAPABILITIES = Object.freeze([
|
|
115
125
|
'fifo-retry-fence',
|
|
@@ -2123,25 +2133,58 @@ const collectActiveTombstoneIds = (state = {}, kind) => {
|
|
|
2123
2133
|
};
|
|
2124
2134
|
|
|
2125
2135
|
const mergeLifecycleTombstoneDocuments = (current = {}, incoming = {}) => {
|
|
2126
|
-
const merged = {
|
|
2127
|
-
|
|
2128
|
-
const
|
|
2136
|
+
const merged = {};
|
|
2137
|
+
const writeTombstone = (rawId, tombstone) => {
|
|
2138
|
+
const documentKey = lifecycleTombstoneDocumentKey(rawId || tombstone?.id);
|
|
2139
|
+
if (!documentKey || !tombstone) return;
|
|
2140
|
+
const logicalId = decodeURIComponent(documentKey);
|
|
2141
|
+
lifecycleTombstoneKeyAliases(logicalId).forEach((alias) => {
|
|
2142
|
+
delete merged[alias];
|
|
2143
|
+
});
|
|
2144
|
+
const existing = merged[documentKey];
|
|
2145
|
+
const nextTombstone = {...tombstone, id: logicalId};
|
|
2129
2146
|
if (!existing) {
|
|
2130
|
-
merged[
|
|
2147
|
+
merged[documentKey] = nextTombstone;
|
|
2131
2148
|
return;
|
|
2132
2149
|
}
|
|
2133
2150
|
const currentGeneration = Number(existing.lifecycleGeneration || 0);
|
|
2134
|
-
const nextGeneration = Number(
|
|
2151
|
+
const nextGeneration = Number(nextTombstone.lifecycleGeneration || 0);
|
|
2135
2152
|
const currentDeletedAt = Number(existing.deletedAt || 0);
|
|
2136
|
-
const nextDeletedAt = Number(
|
|
2153
|
+
const nextDeletedAt = Number(nextTombstone.deletedAt || 0);
|
|
2137
2154
|
if (nextGeneration > currentGeneration ||
|
|
2138
2155
|
(nextGeneration === currentGeneration && nextDeletedAt >= currentDeletedAt)) {
|
|
2139
|
-
merged[
|
|
2156
|
+
merged[documentKey] = nextTombstone;
|
|
2157
|
+
} else {
|
|
2158
|
+
merged[documentKey] = existing;
|
|
2140
2159
|
}
|
|
2141
|
-
}
|
|
2160
|
+
};
|
|
2161
|
+
Object.entries(current || {}).forEach(([id, tombstone]) => writeTombstone(id, tombstone));
|
|
2162
|
+
Object.entries(incoming || {}).forEach(([id, tombstone]) => writeTombstone(id, tombstone));
|
|
2142
2163
|
return merged;
|
|
2143
2164
|
};
|
|
2144
2165
|
|
|
2166
|
+
const pickNewerLifecycleEntity = (left, right, getTime) => {
|
|
2167
|
+
if (!left) return right;
|
|
2168
|
+
if (!right) return left;
|
|
2169
|
+
const leftGeneration = Number(left.lifecycleGeneration || 0);
|
|
2170
|
+
const rightGeneration = Number(right.lifecycleGeneration || 0);
|
|
2171
|
+
if (leftGeneration !== rightGeneration) {
|
|
2172
|
+
return rightGeneration > leftGeneration ? right : left;
|
|
2173
|
+
}
|
|
2174
|
+
return getTime(right) >= getTime(left) ? right : left;
|
|
2175
|
+
};
|
|
2176
|
+
|
|
2177
|
+
const tombstoneBlocksEntity = (tombstone, entity, getTime) => {
|
|
2178
|
+
if (!isActiveLifecycleTombstone(tombstone)) return false;
|
|
2179
|
+
if (!entity) return true;
|
|
2180
|
+
const tombstoneGeneration = Number(tombstone.lifecycleGeneration || 0);
|
|
2181
|
+
const entityGeneration = Number(entity.lifecycleGeneration || 0);
|
|
2182
|
+
if (entityGeneration !== tombstoneGeneration) {
|
|
2183
|
+
return tombstoneGeneration > entityGeneration;
|
|
2184
|
+
}
|
|
2185
|
+
return toFiniteTimestamp(tombstone.deletedAt) >= getTime(entity);
|
|
2186
|
+
};
|
|
2187
|
+
|
|
2145
2188
|
const collectDocumentIdChanges = (currentDocs = {}, nextDocs = {}) => {
|
|
2146
2189
|
const currentIds = new Set(Object.keys(currentDocs || {}));
|
|
2147
2190
|
const nextIds = new Set(Object.keys(nextDocs || {}));
|
|
@@ -2164,45 +2207,92 @@ export const applyRecordTimeLabelSnapshot = (
|
|
|
2164
2207
|
const incoming = normalizeState(incomingState || {});
|
|
2165
2208
|
const currentDocs = buildFirestoreV2DocumentsFromState(current);
|
|
2166
2209
|
const incomingDocs = buildFirestoreV2DocumentsFromState(incoming);
|
|
2167
|
-
const
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
? mergedFolderDocs
|
|
2188
|
-
: Object.fromEntries(Object.entries(mergedFolderDocs).filter(([id]) => (
|
|
2189
|
-
incomingDocs.folders?.[id] && !blockedFolderIds.has(id)
|
|
2190
|
-
)));
|
|
2191
|
-
|
|
2192
|
-
const nextRoot = buildFirestoreV2SnapshotRoot({
|
|
2193
|
-
currentRoot: currentDocs.root || {},
|
|
2194
|
-
incomingRoot: incomingDocs.root || {},
|
|
2195
|
-
mode: snapshotMode
|
|
2210
|
+
const mergedTombstoneDocs = mergeLifecycleTombstoneDocuments(
|
|
2211
|
+
currentDocs.lifecycleTombstones,
|
|
2212
|
+
incomingDocs.lifecycleTombstones
|
|
2213
|
+
);
|
|
2214
|
+
const splitTombstones = splitLifecycleTombstoneDocuments(mergedTombstoneDocs);
|
|
2215
|
+
const mergedRecordTombstones = mergeLifecycleTombstoneSources(
|
|
2216
|
+
current.deletedRecordTombstones,
|
|
2217
|
+
mergeLifecycleTombstoneSources(incoming.deletedRecordTombstones, splitTombstones.records)
|
|
2218
|
+
);
|
|
2219
|
+
const mergedFolderTombstones = mergeLifecycleTombstoneSources(
|
|
2220
|
+
current.deletedFolderTombstones,
|
|
2221
|
+
mergeLifecycleTombstoneSources(incoming.deletedFolderTombstones, splitTombstones.folders)
|
|
2222
|
+
);
|
|
2223
|
+
const remainingTombstones = dropSupersededLifecycleDeletions({
|
|
2224
|
+
deletedRecordTombstones: mergedRecordTombstones,
|
|
2225
|
+
deletedFolderTombstones: mergedFolderTombstones,
|
|
2226
|
+
trashEntries: {},
|
|
2227
|
+
activeGenerations: collectActiveLifecycleStates(
|
|
2228
|
+
...(isMerge ? [current, incoming] : [incoming])
|
|
2229
|
+
)
|
|
2196
2230
|
});
|
|
2231
|
+
|
|
2232
|
+
const selectRecordDocs = (currentRecordDocs, incomingRecordDocs) => {
|
|
2233
|
+
const sourceIds = isMerge
|
|
2234
|
+
? new Set([...Object.keys(currentRecordDocs || {}), ...Object.keys(incomingRecordDocs || {})])
|
|
2235
|
+
: new Set(Object.keys(incomingRecordDocs || {}));
|
|
2236
|
+
const nextDocs = {};
|
|
2237
|
+
sourceIds.forEach((id) => {
|
|
2238
|
+
const entity = isMerge
|
|
2239
|
+
? pickNewerLifecycleEntity(currentRecordDocs?.[id], incomingRecordDocs?.[id], getRecordTime)
|
|
2240
|
+
: incomingRecordDocs?.[id];
|
|
2241
|
+
if (!entity || tombstoneBlocksEntity(remainingTombstones.deletedRecordTombstones[id], entity, getRecordTime)) {
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
nextDocs[id] = entity;
|
|
2245
|
+
});
|
|
2246
|
+
return nextDocs;
|
|
2247
|
+
};
|
|
2248
|
+
const selectFolderDocs = (currentFolderDocs, incomingFolderDocs) => {
|
|
2249
|
+
const sourceIds = isMerge
|
|
2250
|
+
? new Set([...Object.keys(currentFolderDocs || {}), ...Object.keys(incomingFolderDocs || {})])
|
|
2251
|
+
: new Set(Object.keys(incomingFolderDocs || {}));
|
|
2252
|
+
const nextDocs = {};
|
|
2253
|
+
sourceIds.forEach((id) => {
|
|
2254
|
+
const entity = isMerge
|
|
2255
|
+
? pickNewerLifecycleEntity(currentFolderDocs?.[id], incomingFolderDocs?.[id], getFolderTime)
|
|
2256
|
+
: incomingFolderDocs?.[id];
|
|
2257
|
+
if (!entity || tombstoneBlocksEntity(remainingTombstones.deletedFolderTombstones[id], entity, getFolderTime)) {
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
nextDocs[id] = entity;
|
|
2261
|
+
});
|
|
2262
|
+
return nextDocs;
|
|
2263
|
+
};
|
|
2264
|
+
|
|
2265
|
+
const recordDocs = selectRecordDocs(currentDocs.records, incomingDocs.records);
|
|
2266
|
+
const folderDocs = selectFolderDocs(currentDocs.folders, incomingDocs.folders);
|
|
2267
|
+
const blockedRecordIds = new Set(
|
|
2268
|
+
Object.keys(remainingTombstones.deletedRecordTombstones || {}).filter((id) => (
|
|
2269
|
+
isActiveLifecycleTombstone(remainingTombstones.deletedRecordTombstones[id]) && !recordDocs[id]
|
|
2270
|
+
))
|
|
2271
|
+
);
|
|
2272
|
+
const blockedFolderIds = new Set(
|
|
2273
|
+
Object.keys(remainingTombstones.deletedFolderTombstones || {}).filter((id) => (
|
|
2274
|
+
isActiveLifecycleTombstone(remainingTombstones.deletedFolderTombstones[id]) && !folderDocs[id]
|
|
2275
|
+
))
|
|
2276
|
+
);
|
|
2277
|
+
|
|
2278
|
+
const nextRoot = {
|
|
2279
|
+
...buildFirestoreV2SnapshotRoot({
|
|
2280
|
+
currentRoot: currentDocs.root || {},
|
|
2281
|
+
incomingRoot: incomingDocs.root || {},
|
|
2282
|
+
mode: snapshotMode
|
|
2283
|
+
}),
|
|
2284
|
+
deletedRecordTombstones: remainingTombstones.deletedRecordTombstones,
|
|
2285
|
+
deletedFolderTombstones: remainingTombstones.deletedFolderTombstones
|
|
2286
|
+
};
|
|
2197
2287
|
const {state} = buildStateFromFirestoreV2Documents({
|
|
2198
2288
|
root: {id: 'main', ...nextRoot},
|
|
2199
2289
|
records: recordDocs,
|
|
2200
2290
|
folders: folderDocs,
|
|
2201
2291
|
trash: {...(currentDocs.trash || {})},
|
|
2202
2292
|
ops: {},
|
|
2203
|
-
lifecycleTombstones:
|
|
2204
|
-
|
|
2205
|
-
|
|
2293
|
+
lifecycleTombstones: buildV2LifecycleTombstoneDocuments(
|
|
2294
|
+
remainingTombstones.deletedRecordTombstones,
|
|
2295
|
+
remainingTombstones.deletedFolderTombstones
|
|
2206
2296
|
)
|
|
2207
2297
|
});
|
|
2208
2298
|
const nextDocs = buildFirestoreV2DocumentsFromState(state);
|
|
@@ -5547,6 +5637,32 @@ export default {
|
|
|
5547
5637
|
RECORD_TIMELABEL_CORE_VERSION,
|
|
5548
5638
|
RECORD_GROUP_IDENTITY_VERSION,
|
|
5549
5639
|
RECORD_TIMELABEL_DOMAIN_CAPABILITIES,
|
|
5640
|
+
buildLegacyRecordTimeLabelGroupId,
|
|
5641
|
+
buildRecordTimeLabelGroupIndex,
|
|
5642
|
+
buildRecordTimeLabelGroupMetadata,
|
|
5643
|
+
buildRecordTimeLabelGroupReorderOperation,
|
|
5644
|
+
buildRecordTimeLabelGroupReorderOperationId,
|
|
5645
|
+
buildRecordTimeLabelStandaloneGroupId,
|
|
5646
|
+
canonicalizeRecordTimeLabelGroupView,
|
|
5647
|
+
extractTwitchVodIdFromUrl,
|
|
5648
|
+
extractYouTubeVideoIdFromUrl,
|
|
5649
|
+
buildKnownTwitchVodRecordPatches,
|
|
5650
|
+
buildRecordTimeLabelVodPatchOperation,
|
|
5651
|
+
buildRecordTimeLabelVodPatchOperationId,
|
|
5652
|
+
calculateVodTitleSimilarity,
|
|
5653
|
+
normalizeVodTitleForMatch,
|
|
5654
|
+
scoreTwitchVodCandidate,
|
|
5655
|
+
selectBestMatchingTwitchVod,
|
|
5656
|
+
normalizeLegacyRecordTimeLabelImport,
|
|
5657
|
+
remapLegacyRecordTimeLabelFolderAliases,
|
|
5658
|
+
buildRecordTimeLabelChannelFolderPlan,
|
|
5659
|
+
collectRecordTimeLabelChannelCandidates,
|
|
5660
|
+
createdAtForStableRecordTimeLabelOperationId,
|
|
5661
|
+
extractChannelHandleFromUrl,
|
|
5662
|
+
findBestRecordTimeLabelChannelFolder,
|
|
5663
|
+
getChannelMatchKey,
|
|
5664
|
+
isPendingChannelFolderId,
|
|
5665
|
+
isValidChannelName,
|
|
5550
5666
|
RECORD_TIMELABEL_DURABLE_ENGINE_CAPABILITIES,
|
|
5551
5667
|
RECORD_TIMELABEL_CAPABILITY_LIFECYCLE_GENERATION_FENCE,
|
|
5552
5668
|
RECORD_TIMELABEL_CAPABILITY_OPERATION_CONFLICT_QUARANTINE,
|