@recordtimelabel/core 0.3.3 → 0.4.0

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 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 v2 gateway contract release is:
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 durable identity and explicit entrypoint contract is prepared in package version `0.4.0` (publish it before updating consumers):
21
21
 
22
22
  ```json
23
- "@recordtimelabel/core": "0.3.2"
23
+ "@recordtimelabel/core": "0.4.0"
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.
@@ -32,6 +32,9 @@ If this checkout's `package.json` is ahead of the published version, publish the
32
32
  - `mergeLocalRemote({ localState, remoteState, pendingOps, clientId, now })`
33
33
  - `applyOperation(state, operation)`
34
34
  - `applyRecordTimeLabelOperation(state, operation)`
35
+ - `createRecordTimeLabelSyncEngine({ storage, cloud, session, client, clock, logger })`
36
+ - `normalizeRecordTimeLabelOperationResults(operations, results)`
37
+ - `normalizeRecordTimeLabelEnvelopeResponse(operations, response)`
35
38
  - `createSyncEngine({ storageAdapter, cloudAdapter, clientId, clock, logger })`
36
39
  - `createRecordTimeLabelController({ storageAdapter, cloudAdapter, clientId, settingKeys, clock, logger })`
37
40
  - `buildRecordTimeLabelSyncPayload({ data, previousCloudData, pendingOps, syncMode, clientId, now, groupOrderNormalizer })`
@@ -55,6 +58,8 @@ If this checkout's `package.json` is ahead of the published version, publish the
55
58
  - `RTL_SYNC_PROTOCOL_VERSION`
56
59
  - `hasMeaningfulRecordTimeLabelCloudState(data, options)`
57
60
  - `buildRecordTimeLabelContentFingerprint(data)`
61
+ - `normalizeRecordTimeLabelDomainState(input)`
62
+ - `migrateRecordTimeLabelExpandedGroups({ pendingOperations, currentView })`
58
63
  - `buildMigratedRecordTimeLabelV2State(legacyState, options)`
59
64
  - `RECORD_TIMELABEL_CLOUD_SCHEMAS`
60
65
  - `RECORD_TIMELABEL_SYNC_MODES`
@@ -63,6 +68,71 @@ If this checkout's `package.json` is ahead of the published version, publish the
63
68
  - `getActiveTrashEntries(entries, now)`
64
69
  - `RTL_TRASH_RETENTION_MS`
65
70
 
71
+ The package exports four intentional entrypoints. The root (`@recordtimelabel/core`)
72
+ keeps the complete backwards-compatible surface, `/protocol` contains only shared
73
+ acknowledgement protocol helpers, `/firestore-v2` contains platform-neutral document
74
+ and operation planners, and `/compat` contains the legacy `createSyncEngine` and
75
+ `createRecordTimeLabelController` APIs.
76
+
77
+ ### Durable sync engine
78
+
79
+ `createRecordTimeLabelSyncEngine` is the platform-neutral durable workspace API. Its storage port
80
+ has `load()` and `save(workspace)` methods (adapters may consume the optional second
81
+ `save(workspace, fenceContext)` argument to enforce the session/epoch atomically), and its cloud port has `bootstrap(context)`,
82
+ `applyOperations(envelope, context)`, and `subscribe(listener, context)` methods. The session port
83
+ provides `current()`, `subscribe(listener)`, and `isCurrent(sessionToken, uid, workspaceEpoch)`.
84
+
85
+ The persisted workspace is versioned and contains only durable data:
86
+
87
+ ```js
88
+ {
89
+ schemaVersion: 1,
90
+ ownerUid: 'user-id',
91
+ workspaceEpoch: 3,
92
+ remoteBaseline: { state, revision: 12, changeCursor: 'cursor-12' },
93
+ pendingOperations: [],
94
+ rejectedOperations: {},
95
+ syncMeta: {}
96
+ }
97
+ ```
98
+
99
+ Every normalized pending operation also carries `ownerUid` and a non-negative
100
+ `workspaceEpoch`. Dispatch stamps these fields from the captured session; legacy
101
+ operations missing them inherit the migrated workspace identity. An operation whose
102
+ explicit identity does not match the current workspace is moved to
103
+ `rejectedOperations` with a stable identity-mismatch reason before any cloud apply.
104
+
105
+ The engine exposes `init()`, `dispatch(operations)`, `sync(reason)`, `getSnapshot()`,
106
+ `subscribe(listener)`, and `destroy()`. `getSnapshot().state` is derived by replaying pending
107
+ operations over `remoteBaseline.state`; rejected operations are kept in diagnostics and are not
108
+ replayed. Session tokens are used for fencing but are never persisted. The legacy
109
+ `createSyncEngine` and app adapters remain available and are not implicitly migrated by this API.
110
+
111
+ `expandedGroups` is a local view projection, not durable cloud domain state. Durable workspaces,
112
+ content fingerprints, and Firestore v2 root documents exclude it. The migration helper consumes
113
+ legacy `expandedGroups.update` operations in order, returns the resulting view projection, and is
114
+ idempotent; legacy root fields are preserved verbatim when a compatibility document is rewritten
115
+ so older clients can continue reading the view until they migrate.
116
+
117
+ ### Operation-result protocol
118
+
119
+ The root package and `@recordtimelabel/core/protocol` export the shared acknowledgement
120
+ normalizers. `normalizeRecordTimeLabelOperationResults(operations, results)` always returns one
121
+ positional result per submitted operation. The only normalized statuses are `applied`, `noop`,
122
+ `rejected`, and `retryable`; `retryable` is derived from that status, while the legacy `id` and
123
+ `applied` fields remain available alongside `operationId`, `reason`, and `retryAfterMs`.
124
+
125
+ `normalizeRecordTimeLabelEnvelopeResponse(operations, response)` prefers an explicit
126
+ `operationResults` array. An explicitly provided non-array or mismatched array, and a failed
127
+ envelope without results, throw an error whose `code` is `operation_result_count_mismatch`, so
128
+ callers can reject the entire acknowledgement before changing durable state.
129
+
130
+ During the compatibility window, a successful legacy envelope that omits the
131
+ `operationResults` property acknowledges every submitted operation as `applied`. Failed envelopes
132
+ and envelopes that explicitly provide a malformed result value never receive that fallback. Keep
133
+ the fallback until all deployed gateways and clients send and consume the explicit array; removing
134
+ it requires a coordinated breaking release.
135
+
66
136
  ## Firestore v1 Compatibility
67
137
 
68
138
  Phase 1 keeps the existing `users/{uid}` document shape:
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@recordtimelabel/core",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Shared RecordTimeLabel data model, merge logic, operations, and sync engine.",
6
6
  "main": "./src/index.js",
7
7
  "exports": {
8
- ".": "./src/index.js"
8
+ ".": "./src/index.js",
9
+ "./protocol": "./src/protocol.js",
10
+ "./firestore-v2": "./src/firestore-v2.js",
11
+ "./compat": "./src/compat.js"
9
12
  },
10
13
  "scripts": {
11
14
  "test": "node --test tests/*.test.mjs"
package/src/compat.js ADDED
@@ -0,0 +1,8 @@
1
+ // Compatibility surface for the pre-durable sync/controller APIs. Keeping
2
+ // these exports explicit lets adapters migrate without pulling in unrelated
3
+ // Firestore-v2 planners.
4
+ export {createRecordTimeLabelController, createSyncEngine} from './index.js';
5
+
6
+ import {createRecordTimeLabelController, createSyncEngine} from './index.js';
7
+
8
+ export default {createSyncEngine, createRecordTimeLabelController};
@@ -0,0 +1,27 @@
1
+ // Platform-neutral Firestore v2 planning and document helpers. Runtime
2
+ // adapters (Firebase SDKs, browser storage, and UI bindings) belong outside
3
+ // the core package and should import this explicit surface.
4
+ export {
5
+ FIRESTORE_V2_SETTINGS_DOC_ID,
6
+ OPERATION_TYPES,
7
+ RECORD_TIMELABEL_CLOUD_SCHEMAS,
8
+ RECORD_TIMELABEL_OPERATION_RESULT_STATUSES,
9
+ RTL_MAX_OPERATIONS_PER_REQUEST,
10
+ RTL_MAX_REQUEST_BYTES,
11
+ RTL_MAX_TARGET_WRITES,
12
+ RTL_SYNC_PROTOCOL_VERSION,
13
+ buildFirestoreV2DocumentChangeSet,
14
+ buildFirestoreV2DocumentsFromState,
15
+ buildFirestoreV2LogicalPaths,
16
+ buildFirestoreV2OperationReadPlan,
17
+ buildOperationsFromSnapshotDiff,
18
+ buildStateFromFirestoreV2Documents,
19
+ estimateFirestoreV2WriteUnits,
20
+ extendFirestoreV2OperationReadPlanWithRecords,
21
+ extendFirestoreV2OperationReadPlanWithTrash,
22
+ normalizeRecordTimeLabelEnvelopeResponse,
23
+ normalizeRecordTimeLabelOperationResults,
24
+ planFirestoreV2OperationChanges,
25
+ normalizeRecordTimeLabelDomainState,
26
+ validateRecordTimeLabelOperationBatch
27
+ } from './index.js';