@recordtimelabel/core 0.6.2 → 0.6.4
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 +18 -10
- package/package.json +1 -1
- package/src/changefeed.js +67 -8
- package/src/firestore-v2.js +11 -0
- package/src/index.js +1562 -294
- package/src/protocol.js +429 -104
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
|
|
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.4`; 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.4"
|
|
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.
|
|
@@ -106,14 +106,22 @@ the whole session object as an authorization token, and `bootstrap`/`catchUp`/`a
|
|
|
106
106
|
context no longer carries `token`. Platform adapters obtain Firebase credentials from
|
|
107
107
|
their own credential provider.
|
|
108
108
|
|
|
109
|
-
`normalizeRecordTimeLabelCloudFailure` preserves `status`, `reason`, `
|
|
110
|
-
`retryAfterMs`, and assigns exactly one class:
|
|
111
|
-
`auth-transition-required`, `terminal`, or
|
|
112
|
-
`
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
`normalizeRecordTimeLabelCloudFailure` preserves `status`, `reason`, `code`,
|
|
110
|
+
`message`, `retryable`, and `retryAfterMs`, and assigns exactly one class:
|
|
111
|
+
`transient`, `bootstrap-required`, `auth-transition-required`, `terminal`, or
|
|
112
|
+
`stale-session`. Classification uses only an explicit class, HTTP status,
|
|
113
|
+
`retryable`, `bootstrapRequired`, and other platform-neutral fields. Adapters
|
|
114
|
+
may keep a Firebase reason string, but Core does not interpret Firebase or JWT
|
|
115
|
+
taxonomies. `{error: "reason"}` keeps that string as `reason`, `code`, and
|
|
116
|
+
`message`. Capability `cloud-failure-state-v1` is part of
|
|
117
|
+
`RECORD_TIMELABEL_PROTOCOL_CAPABILITIES`; clients must not invent that string.
|
|
118
|
+
An `auth-transition-required` bootstrap or catch-up failure latches the captured
|
|
119
|
+
session identity (`sessionToken` + UID + workspace epoch) so repeated `init()`
|
|
120
|
+
calls do not invoke `cloud.bootstrap` again. A changed SessionPort identity,
|
|
121
|
+
UID, or workspace epoch clears the latch. An ID token refresh that keeps the
|
|
122
|
+
same `sessionToken` must not change session identity or current work.
|
|
123
|
+
`recoverBaseline` falls back from catch-up only for explicit
|
|
124
|
+
`bootstrap-required` / cursor-gap outcomes; auth and transient failures keep
|
|
117
125
|
their class and must not start a second full walk.
|
|
118
126
|
|
|
119
127
|
The persisted workspace is versioned and contains only durable data:
|
package/package.json
CHANGED
package/src/changefeed.js
CHANGED
|
@@ -72,15 +72,38 @@ export const applyFirestoreV2ResolvedChangeBatch = ({
|
|
|
72
72
|
const inputRevision = Number(cache?.revision || 0);
|
|
73
73
|
const inputDocuments = cache?.documents || {};
|
|
74
74
|
const inputCache = {revision: inputRevision, documents: inputDocuments};
|
|
75
|
-
const
|
|
76
|
-
|
|
75
|
+
const hasExpectedTarget = targetRevision != null;
|
|
76
|
+
const expectedTarget = hasExpectedTarget ? Number(targetRevision) : null;
|
|
77
|
+
if (!Number.isSafeInteger(inputRevision) || inputRevision < 0 ||
|
|
78
|
+
(hasExpectedTarget && (!Number.isSafeInteger(expectedTarget) || expectedTarget < 0))) {
|
|
77
79
|
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.REVISION_GAP, inputCache);
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
const batch = Array.isArray(changes) ? changes : [];
|
|
81
83
|
if (batch.length === 0) {
|
|
82
|
-
if (expectedTarget != null && Number.isFinite(expectedTarget)
|
|
83
|
-
|
|
84
|
+
if (expectedTarget != null && Number.isFinite(expectedTarget)) {
|
|
85
|
+
if (expectedTarget < inputRevision) {
|
|
86
|
+
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.CACHE_AHEAD, inputCache);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const resolvedRoot = resolvedDocuments?.root;
|
|
90
|
+
if (hasExpectedTarget || resolvedRoot !== undefined) {
|
|
91
|
+
const rootRevision = Number(resolvedRoot?.revision);
|
|
92
|
+
const expectedRootRevision = expectedTarget != null && Number.isFinite(expectedTarget)
|
|
93
|
+
? expectedTarget
|
|
94
|
+
: inputRevision;
|
|
95
|
+
if (!resolvedRoot || !Number.isSafeInteger(rootRevision) || rootRevision < 0) {
|
|
96
|
+
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.ROOT_DOCUMENT_MISSING, inputCache);
|
|
97
|
+
}
|
|
98
|
+
if (rootRevision < expectedRootRevision) {
|
|
99
|
+
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.ROOT_DOCUMENT_MISSING, inputCache);
|
|
100
|
+
}
|
|
101
|
+
if (rootRevision > expectedRootRevision) {
|
|
102
|
+
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.REVISION_OVERSHOOT, inputCache);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (expectedTarget != null && Number.isFinite(expectedTarget) && expectedTarget > inputRevision) {
|
|
106
|
+
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.REVISION_GAP, inputCache);
|
|
84
107
|
}
|
|
85
108
|
return {
|
|
86
109
|
ok: true,
|
|
@@ -92,19 +115,30 @@ export const applyFirestoreV2ResolvedChangeBatch = ({
|
|
|
92
115
|
}
|
|
93
116
|
|
|
94
117
|
const firstRevision = Number(batch[0]?.revision || 0);
|
|
95
|
-
if (
|
|
118
|
+
if (!Number.isSafeInteger(firstRevision) || firstRevision < 0) {
|
|
119
|
+
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.REVISION_GAP, inputCache);
|
|
120
|
+
}
|
|
121
|
+
if (expectedTarget != null && inputRevision > expectedTarget) {
|
|
96
122
|
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.CACHE_AHEAD, inputCache);
|
|
97
123
|
}
|
|
98
124
|
if (firstRevision <= inputRevision) {
|
|
99
125
|
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.REVISION_DUPLICATE, inputCache);
|
|
100
126
|
}
|
|
101
|
-
if (expectedTarget != null &&
|
|
127
|
+
if (expectedTarget != null && firstRevision > expectedTarget) {
|
|
102
128
|
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.REVISION_OVERSHOOT, inputCache);
|
|
103
129
|
}
|
|
104
130
|
if (firstRevision > inputRevision + 1) {
|
|
105
131
|
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.REVISION_GAP, inputCache);
|
|
106
132
|
}
|
|
107
133
|
|
|
134
|
+
// Keep the last applied revision available for root exactness when the
|
|
135
|
+
// caller did not provide a target. A malformed or stale final entry is
|
|
136
|
+
// rejected before candidate mutation.
|
|
137
|
+
const finalRevision = Number(batch.at(-1)?.revision || 0);
|
|
138
|
+
if (!Number.isSafeInteger(finalRevision) || finalRevision < inputRevision) {
|
|
139
|
+
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.REVISION_DUPLICATE, inputCache);
|
|
140
|
+
}
|
|
141
|
+
|
|
108
142
|
const candidate = {
|
|
109
143
|
revision: inputRevision,
|
|
110
144
|
documents: cloneDocuments(inputDocuments)
|
|
@@ -114,13 +148,16 @@ export const applyFirestoreV2ResolvedChangeBatch = ({
|
|
|
114
148
|
for (let index = 0; index < batch.length; index += 1) {
|
|
115
149
|
const change = batch[index] || {};
|
|
116
150
|
const revision = Number(change.revision || 0);
|
|
151
|
+
if (!Number.isSafeInteger(revision) || revision < 0) {
|
|
152
|
+
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.REVISION_GAP, inputCache);
|
|
153
|
+
}
|
|
117
154
|
if (revision !== candidate.revision + 1) {
|
|
118
155
|
const reason = revision <= candidate.revision
|
|
119
156
|
? FIRESTORE_V2_BOOTSTRAP_REASONS.REVISION_DUPLICATE
|
|
120
157
|
: FIRESTORE_V2_BOOTSTRAP_REASONS.REVISION_GAP;
|
|
121
158
|
return fail(reason, inputCache);
|
|
122
159
|
}
|
|
123
|
-
if (expectedTarget != null &&
|
|
160
|
+
if (expectedTarget != null && revision > expectedTarget) {
|
|
124
161
|
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.REVISION_OVERSHOOT, inputCache);
|
|
125
162
|
}
|
|
126
163
|
|
|
@@ -165,7 +202,8 @@ export const applyFirestoreV2ResolvedChangeBatch = ({
|
|
|
165
202
|
if (change.rootChanged === true) {
|
|
166
203
|
const rootDocument = resolvedDocuments.root;
|
|
167
204
|
const rootRevision = Number(rootDocument?.revision || 0);
|
|
168
|
-
if (!rootDocument || !Number.
|
|
205
|
+
if (!rootDocument || !Number.isSafeInteger(rootRevision) || rootRevision < 0 ||
|
|
206
|
+
rootRevision < revision) {
|
|
169
207
|
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.ROOT_DOCUMENT_MISSING, inputCache);
|
|
170
208
|
}
|
|
171
209
|
rootChanged = true;
|
|
@@ -206,6 +244,27 @@ export const applyFirestoreV2ResolvedChangeBatch = ({
|
|
|
206
244
|
candidate.revision = revision;
|
|
207
245
|
}
|
|
208
246
|
|
|
247
|
+
// When a change resolves the root (or a caller supplied one alongside the
|
|
248
|
+
// batch), its revision is the proof that all document reads belong to the
|
|
249
|
+
// same authoritative point. A behind, missing, or overshooting root is a
|
|
250
|
+
// bootstrap condition; the candidate is discarded atomically.
|
|
251
|
+
const resolvedRoot = resolvedDocuments?.root;
|
|
252
|
+
if (hasExpectedTarget || rootChanged || resolvedRoot !== undefined) {
|
|
253
|
+
const rootRevision = Number(resolvedRoot?.revision);
|
|
254
|
+
const expectedRootRevision = expectedTarget != null && Number.isFinite(expectedTarget)
|
|
255
|
+
? expectedTarget
|
|
256
|
+
: finalRevision;
|
|
257
|
+
if (!resolvedRoot || !Number.isSafeInteger(rootRevision) || rootRevision < 0) {
|
|
258
|
+
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.ROOT_DOCUMENT_MISSING, inputCache);
|
|
259
|
+
}
|
|
260
|
+
if (rootRevision < expectedRootRevision) {
|
|
261
|
+
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.ROOT_DOCUMENT_MISSING, inputCache);
|
|
262
|
+
}
|
|
263
|
+
if (rootRevision > expectedRootRevision) {
|
|
264
|
+
return fail(FIRESTORE_V2_BOOTSTRAP_REASONS.REVISION_OVERSHOOT, inputCache);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
209
268
|
if (rootChanged) {
|
|
210
269
|
const rootDocument = resolvedDocuments.root;
|
|
211
270
|
candidate.documents.root = {id: 'main', ...rootDocument};
|
package/src/firestore-v2.js
CHANGED
|
@@ -7,12 +7,21 @@ export {
|
|
|
7
7
|
RECORD_TIMELABEL_CAPABILITY_LIFECYCLE_GENERATION_FENCE,
|
|
8
8
|
RECORD_TIMELABEL_CAPABILITY_OPERATION_CONFLICT_QUARANTINE,
|
|
9
9
|
RECORD_TIMELABEL_CAPABILITY_STRICT_OPERATION_RESULTS,
|
|
10
|
+
RECORD_TIMELABEL_CAPABILITY_DETERMINISTIC_PLANNER,
|
|
11
|
+
RECORD_TIMELABEL_CAPABILITY_STRICT_READINESS,
|
|
12
|
+
RECORD_TIMELABEL_CAPABILITY_LIFECYCLE_GENERATION_FENCE_V1,
|
|
13
|
+
RECORD_TIMELABEL_CLOUD_FAILURE_SCHEMA_VERSION,
|
|
10
14
|
RECORD_TIMELABEL_CLOUD_SCHEMAS,
|
|
11
15
|
RECORD_TIMELABEL_OPERATION_RESULT_STATUSES,
|
|
12
16
|
RECORD_TIMELABEL_PROTOCOL_CAPABILITIES,
|
|
13
17
|
RTL_MAX_OPERATIONS_PER_REQUEST,
|
|
14
18
|
RTL_MAX_REQUEST_BYTES,
|
|
15
19
|
RTL_MAX_TARGET_WRITES,
|
|
20
|
+
RECORD_TIMELABEL_PLANNER_MODES,
|
|
21
|
+
RECORD_TIMELABEL_SAFE_ID_MAX_BYTES,
|
|
22
|
+
isRecordTimeLabelSafeDocumentId,
|
|
23
|
+
normalizeRecordTimeLabelImmutableId,
|
|
24
|
+
normalizeRecordTimeLabelPlannerId,
|
|
16
25
|
RTL_SYNC_PROTOCOL_VERSION,
|
|
17
26
|
buildFirestoreV2DocumentChangeSet,
|
|
18
27
|
buildFirestoreV2DocumentsFromState,
|
|
@@ -33,6 +42,8 @@ export {
|
|
|
33
42
|
normalizeRecordTimeLabelEnvelopeResponse,
|
|
34
43
|
normalizeRecordTimeLabelOperationResults,
|
|
35
44
|
planFirestoreV2OperationChanges,
|
|
45
|
+
planRecordTimeLabelDeterministicOperationChanges,
|
|
46
|
+
planFirestoreV2DeterministicOperationChanges,
|
|
36
47
|
toRecordTimeLabelWireOperation,
|
|
37
48
|
normalizeRecordTimeLabelDomainState,
|
|
38
49
|
validateRecordTimeLabelOperationBatch
|