@recordtimelabel/core 0.6.7 → 0.6.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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/changefeed.js +31 -1
- package/src/index.js +54 -23
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 release target 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 release target is `0.6.9`; verify that its registry tarball and lockfile integrity are available before updating consumers:
|
|
21
21
|
|
|
22
22
|
```json
|
|
23
|
-
"@recordtimelabel/core": "0.6.
|
|
23
|
+
"@recordtimelabel/core": "0.6.9"
|
|
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
|
@@ -145,6 +145,33 @@ export const applyFirestoreV2ResolvedChangeBatch = ({
|
|
|
145
145
|
};
|
|
146
146
|
let rootChanged = false;
|
|
147
147
|
|
|
148
|
+
// Resolved documents represent the final target revision, not every
|
|
149
|
+
// intermediate receipt. If an entity is changed and then deleted within
|
|
150
|
+
// this contiguous batch, its final document correctly does not exist.
|
|
151
|
+
// Record the final mutation so that specific case can be reduced without
|
|
152
|
+
// weakening the fail-closed rule for genuinely missing changed documents.
|
|
153
|
+
const finalMutations = {
|
|
154
|
+
records: new Map(),
|
|
155
|
+
folders: new Map(),
|
|
156
|
+
trash: new Map()
|
|
157
|
+
};
|
|
158
|
+
batch.forEach((change, index) => {
|
|
159
|
+
[
|
|
160
|
+
['records', change?.changedRecordIds, 'changed'],
|
|
161
|
+
['records', change?.deletedRecordIds, 'deleted'],
|
|
162
|
+
['folders', change?.changedFolderIds, 'changed'],
|
|
163
|
+
['folders', change?.deletedFolderIds, 'deleted'],
|
|
164
|
+
['trash', change?.changedTrashEntryIds, 'changed'],
|
|
165
|
+
['trash', change?.deletedTrashEntryIds, 'deleted']
|
|
166
|
+
].forEach(([kind, ids, mutation]) => {
|
|
167
|
+
toIdList(ids).forEach((id) => finalMutations[kind].set(id, {index, mutation}));
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
const isShadowedByLaterDelete = (kind, id, index) => {
|
|
171
|
+
const finalMutation = finalMutations[kind].get(id);
|
|
172
|
+
return finalMutation?.mutation === 'deleted' && finalMutation.index > index;
|
|
173
|
+
};
|
|
174
|
+
|
|
148
175
|
for (let index = 0; index < batch.length; index += 1) {
|
|
149
176
|
const change = batch[index] || {};
|
|
150
177
|
const revision = Number(change.revision || 0);
|
|
@@ -193,7 +220,7 @@ export const applyFirestoreV2ResolvedChangeBatch = ({
|
|
|
193
220
|
...changedTrash.map((id) => ['trash', id])
|
|
194
221
|
].some(([kind, id]) => {
|
|
195
222
|
const document = resolvedDocument(resolvedDocuments, kind, id);
|
|
196
|
-
return document == null;
|
|
223
|
+
return document == null && !isShadowedByLaterDelete(kind, id, index);
|
|
197
224
|
});
|
|
198
225
|
if (missingChanged) {
|
|
199
226
|
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.CHANGED_DOCUMENT_MISSING, inputCache);
|
|
@@ -211,14 +238,17 @@ export const applyFirestoreV2ResolvedChangeBatch = ({
|
|
|
211
238
|
|
|
212
239
|
changedRecords.forEach((id) => {
|
|
213
240
|
const document = resolvedDocument(resolvedDocuments, 'records', id);
|
|
241
|
+
if (document == null && isShadowedByLaterDelete('records', id, index)) return;
|
|
214
242
|
candidate.documents.records[id] = {id, ...document};
|
|
215
243
|
});
|
|
216
244
|
changedFolders.forEach((id) => {
|
|
217
245
|
const document = resolvedDocument(resolvedDocuments, 'folders', id);
|
|
246
|
+
if (document == null && isShadowedByLaterDelete('folders', id, index)) return;
|
|
218
247
|
candidate.documents.folders[id] = {id, ...document};
|
|
219
248
|
});
|
|
220
249
|
changedTrash.forEach((id) => {
|
|
221
250
|
const document = resolvedDocument(resolvedDocuments, 'trash', id);
|
|
251
|
+
if (document == null && isShadowedByLaterDelete('trash', id, index)) return;
|
|
222
252
|
candidate.documents.trash[id] = {id, ...document};
|
|
223
253
|
});
|
|
224
254
|
deletedRecords.forEach((id) => delete candidate.documents.records[id]);
|
package/src/index.js
CHANGED
|
@@ -145,7 +145,7 @@ export {
|
|
|
145
145
|
selectBestMatchingTwitchVod
|
|
146
146
|
};
|
|
147
147
|
|
|
148
|
-
export const RECORD_TIMELABEL_CORE_VERSION = '0.6.
|
|
148
|
+
export const RECORD_TIMELABEL_CORE_VERSION = '0.6.9';
|
|
149
149
|
export const RTL_SYNC_PROTOCOL_VERSION = 2;
|
|
150
150
|
export const RECORD_TIMELABEL_DURABLE_ENGINE_CAPABILITIES = Object.freeze([
|
|
151
151
|
'fifo-retry-fence',
|
|
@@ -4048,6 +4048,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4048
4048
|
})
|
|
4049
4049
|
};
|
|
4050
4050
|
let bootstrapAttemptSequence = 0;
|
|
4051
|
+
const activeBootstrapAttempts = new Map();
|
|
4051
4052
|
let authTransitionLatch = null;
|
|
4052
4053
|
const listeners = new Set();
|
|
4053
4054
|
let queue = Promise.resolve();
|
|
@@ -4168,7 +4169,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4168
4169
|
return {changed, stale: false};
|
|
4169
4170
|
};
|
|
4170
4171
|
|
|
4171
|
-
const
|
|
4172
|
+
const createBootstrapAttemptOptions = (mode) => ({
|
|
4172
4173
|
mode,
|
|
4173
4174
|
attemptId: `bootstrap:${mode}:${now()}:${++bootstrapAttemptSequence}:${
|
|
4174
4175
|
globalThis.crypto?.randomUUID?.() || Math.random().toString(36).slice(2, 14)
|
|
@@ -4176,6 +4177,27 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4176
4177
|
requireFresh: true
|
|
4177
4178
|
});
|
|
4178
4179
|
|
|
4180
|
+
const bootstrapAttemptIdentity = (context = {}) => JSON.stringify([
|
|
4181
|
+
context.uid ?? null,
|
|
4182
|
+
context.sessionToken ?? null,
|
|
4183
|
+
Number(context.workspaceEpoch || 0)
|
|
4184
|
+
]);
|
|
4185
|
+
|
|
4186
|
+
const bootstrapAttemptOptions = (mode, context) => {
|
|
4187
|
+
const identity = bootstrapAttemptIdentity(context);
|
|
4188
|
+
const active = activeBootstrapAttempts.get(mode);
|
|
4189
|
+
if (active?.identity === identity) return active.options;
|
|
4190
|
+
const options = createBootstrapAttemptOptions(mode);
|
|
4191
|
+
activeBootstrapAttempts.set(mode, {identity, options});
|
|
4192
|
+
return options;
|
|
4193
|
+
};
|
|
4194
|
+
|
|
4195
|
+
const clearBootstrapAttempt = (mode, options) => {
|
|
4196
|
+
if (activeBootstrapAttempts.get(mode)?.options === options) {
|
|
4197
|
+
activeBootstrapAttempts.delete(mode);
|
|
4198
|
+
}
|
|
4199
|
+
};
|
|
4200
|
+
|
|
4179
4201
|
const normalizeBootstrapResponse = (response) => {
|
|
4180
4202
|
if (!rtlEnvelopeSuccess(response)) {
|
|
4181
4203
|
const source = response && typeof response === 'object' ? response : {
|
|
@@ -4200,6 +4222,24 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4200
4222
|
return {state, revision, changeCursor: remote.changeCursor ?? null};
|
|
4201
4223
|
};
|
|
4202
4224
|
|
|
4225
|
+
// A transient failure belongs to the same logical page walk. Keep its
|
|
4226
|
+
// attempt ID so the platform bootstrap owner can resume the signed cursor;
|
|
4227
|
+
// successful and non-transient outcomes retire the attempt.
|
|
4228
|
+
const runBootstrapAttempt = async (context, mode) => {
|
|
4229
|
+
const options = bootstrapAttemptOptions(mode, context);
|
|
4230
|
+
try {
|
|
4231
|
+
const baseline = normalizeBootstrapResponse(await cloud.bootstrap(context, options));
|
|
4232
|
+
clearBootstrapAttempt(mode, options);
|
|
4233
|
+
return baseline;
|
|
4234
|
+
} catch (error) {
|
|
4235
|
+
const failure = normalizeRecordTimeLabelCloudFailure(error);
|
|
4236
|
+
if (failure.class !== RECORD_TIMELABEL_CLOUD_FAILURE_CLASSES.TRANSIENT) {
|
|
4237
|
+
clearBootstrapAttempt(mode, options);
|
|
4238
|
+
}
|
|
4239
|
+
throw error;
|
|
4240
|
+
}
|
|
4241
|
+
};
|
|
4242
|
+
|
|
4203
4243
|
const clearAuthTransitionLatchIfSessionChanged = (captured) => {
|
|
4204
4244
|
if (!authTransitionLatch) return;
|
|
4205
4245
|
if (!rtlSameSessionIdentity(authTransitionLatch, captured)) {
|
|
@@ -4283,7 +4323,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4283
4323
|
const caught = await cloud.catchUp(context, {
|
|
4284
4324
|
fromRevision: workspace.remoteBaseline.revision,
|
|
4285
4325
|
targetRevision,
|
|
4286
|
-
...
|
|
4326
|
+
...createBootstrapAttemptOptions(`${mode}-catchup`)
|
|
4287
4327
|
});
|
|
4288
4328
|
if (rtlEnvelopeSuccess(caught)) {
|
|
4289
4329
|
return requireBootstrapRevision(normalizeBootstrapResponse(caught), targetRevision);
|
|
@@ -4308,10 +4348,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4308
4348
|
throwIfAuthTransitionLatched(captured);
|
|
4309
4349
|
try {
|
|
4310
4350
|
return requireBootstrapRevision(
|
|
4311
|
-
|
|
4312
|
-
context,
|
|
4313
|
-
bootstrapAttemptOptions(mode)
|
|
4314
|
-
)),
|
|
4351
|
+
await runBootstrapAttempt(context, mode),
|
|
4315
4352
|
targetRevision
|
|
4316
4353
|
);
|
|
4317
4354
|
} catch (error) {
|
|
@@ -4585,10 +4622,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4585
4622
|
}
|
|
4586
4623
|
let baseline;
|
|
4587
4624
|
try {
|
|
4588
|
-
baseline =
|
|
4589
|
-
context,
|
|
4590
|
-
bootstrapAttemptOptions('apply-failure-rebase')
|
|
4591
|
-
));
|
|
4625
|
+
baseline = await runBootstrapAttempt(context, 'apply-failure-rebase');
|
|
4592
4626
|
} catch (bootstrapError) {
|
|
4593
4627
|
if (!(await isCurrent(captured))) return {success: false, reason: 'stale_session'};
|
|
4594
4628
|
rethrowClassifiedCloudFailure(captured, bootstrapError);
|
|
@@ -4755,10 +4789,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
4755
4789
|
if (typeof cloud?.bootstrap === 'function') {
|
|
4756
4790
|
try {
|
|
4757
4791
|
freshBaseline = requireBootstrapRevision(
|
|
4758
|
-
|
|
4759
|
-
context,
|
|
4760
|
-
bootstrapAttemptOptions('rejection-rebase')
|
|
4761
|
-
)),
|
|
4792
|
+
await runBootstrapAttempt(context, 'rejection-rebase'),
|
|
4762
4793
|
requiredRevision
|
|
4763
4794
|
);
|
|
4764
4795
|
} catch (bootstrapError) {
|
|
@@ -5253,10 +5284,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
5253
5284
|
});
|
|
5254
5285
|
let bootstrap;
|
|
5255
5286
|
try {
|
|
5256
|
-
bootstrap =
|
|
5257
|
-
context,
|
|
5258
|
-
bootstrapAttemptOptions('initial-hydration')
|
|
5259
|
-
));
|
|
5287
|
+
bootstrap = await runBootstrapAttempt(context, 'initial-hydration');
|
|
5260
5288
|
} catch (error) {
|
|
5261
5289
|
if (!(await isCurrent(captured))) return getSnapshot();
|
|
5262
5290
|
if (persistedHydrationRequired) {
|
|
@@ -5783,10 +5811,7 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
5783
5811
|
let freshBaseline;
|
|
5784
5812
|
try {
|
|
5785
5813
|
freshBaseline = requireBootstrapRevision(
|
|
5786
|
-
|
|
5787
|
-
context,
|
|
5788
|
-
bootstrapAttemptOptions('rejection-rebase')
|
|
5789
|
-
)),
|
|
5814
|
+
await runBootstrapAttempt(context, 'rejection-rebase'),
|
|
5790
5815
|
responseRevision
|
|
5791
5816
|
);
|
|
5792
5817
|
} catch (error) {
|
|
@@ -5934,6 +5959,12 @@ export const createRecordTimeLabelSyncEngine = ({
|
|
|
5934
5959
|
const captured = capture();
|
|
5935
5960
|
clearAuthTransitionLatchIfSessionChanged(captured);
|
|
5936
5961
|
if (initialized && !hydrationRequired && workspaceMatchesSession(captured)) {
|
|
5962
|
+
// A transient listener failure stops only the provider subscription.
|
|
5963
|
+
// Re-entering init for the same owner must reattach that subscription
|
|
5964
|
+
// without discarding the committed workspace or bootstrapping again.
|
|
5965
|
+
if (captured?.uid && !subscriptionContext && typeof cloud?.subscribe === 'function') {
|
|
5966
|
+
startCloudSubscription(captured, workspace);
|
|
5967
|
+
}
|
|
5937
5968
|
return getSnapshot();
|
|
5938
5969
|
}
|
|
5939
5970
|
if (initialized && !hydrationRequired && !workspaceMatchesSession(captured)) {
|