@classytic/repo-core 0.6.1 → 0.7.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/CHANGELOG.md +17 -0
- package/dist/sync/index.d.mts +131 -0
- package/dist/sync/index.mjs +41 -0
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to `@classytic/repo-core` are documented here.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.7.0] - 2026-07-04
|
|
8
|
+
|
|
9
|
+
### Added — `./sync` change-log / cursor contract
|
|
10
|
+
|
|
11
|
+
Storage-agnostic data-sync spine for offline-first + incremental replication:
|
|
12
|
+
`ChangeLogStore`, `ChangeEntry` (tombstone deletes), pull `ChangesPage`
|
|
13
|
+
(opaque exclusive cursors, `hasMore` paging), Replicache-style `PushMutation`
|
|
14
|
+
/ `PushVerdict` (idempotent client mutation ids, server-authoritative
|
|
15
|
+
conflicts), `CursorExpiredError` (compaction horizon → full resync), and the
|
|
16
|
+
`MemoryChangeLogStore` reference impl pinning the contract semantics.
|
|
17
|
+
|
|
18
|
+
Synthesized from CouchDB `_changes` (tombstones/checkpoints), Drive/Graph
|
|
19
|
+
delta APIs (opaque server-issued cursors), Mongo change-stream resume tokens,
|
|
20
|
+
and Replicache push/pull. Contract only — kits (mongokit/prismakit/sqlitekit)
|
|
21
|
+
implement capture + durable stores via repository plugins; arc re-exports the
|
|
22
|
+
surface at `@classytic/arc/sync`; sync HTTP endpoints are an arc module.
|
|
23
|
+
|
|
7
24
|
## [0.6.1] - 2026-07-04
|
|
8
25
|
|
|
9
26
|
### Changed — `TenantConfig` optionals widened to `T | undefined` (P10)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
//#region src/sync/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Change-log / cursor contract — the data-sync spine for offline-first and
|
|
4
|
+
* incremental replication (`arc-sync`-style capability packages, POS offline,
|
|
5
|
+
* bulk delta export, cross-service read models).
|
|
6
|
+
*
|
|
7
|
+
* Synthesized from the proven protocols, taking the best of each:
|
|
8
|
+
*
|
|
9
|
+
* - CouchDB `_changes` → append-only feed, TOMBSTONES for deletes,
|
|
10
|
+
* client checkpoints
|
|
11
|
+
* - Drive/Graph delta APIs → OPAQUE server-issued cursors (`deltaLink` /
|
|
12
|
+
* `startPageToken`) — clients never parse them
|
|
13
|
+
* - Mongo change streams → resumable, ordered, per-scope feeds
|
|
14
|
+
* - Replicache push/pull → client mutation ids for idempotent PUSH,
|
|
15
|
+
* server-authoritative conflict verdicts
|
|
16
|
+
*
|
|
17
|
+
* CONTRACT ONLY — storage-agnostic (repo-core is the cross-kit contract home,
|
|
18
|
+
* like DataAdapter/EventTransport/IdempotencyStore). `MemoryChangeLogStore` is
|
|
19
|
+
* the reference impl. Durable stores + capture plugins live in the KITS
|
|
20
|
+
* (mongokit/prismakit/sqlitekit repository plugins); the HTTP surface
|
|
21
|
+
* (`/sync/pull`, `/sync/push`) is an arc module; arc re-exports this contract
|
|
22
|
+
* at `@classytic/arc/sync`. repo-core is source of truth.
|
|
23
|
+
*
|
|
24
|
+
* ## Required semantics (stores MUST honor)
|
|
25
|
+
*
|
|
26
|
+
* 1. **Cursors are opaque, totally ordered per store.** Clients echo them
|
|
27
|
+
* verbatim. A cursor from one store/scope-set is meaningless elsewhere.
|
|
28
|
+
* 2. **`append` is ordered and atomic with the business write** when a
|
|
29
|
+
* `session` is provided (same transaction — the outbox discipline).
|
|
30
|
+
* 3. **Deletes are tombstones**, never gaps. A client at cursor C must be able
|
|
31
|
+
* to converge by applying every entry after C — including deletions.
|
|
32
|
+
* 4. **`since` is exclusive** of the given cursor and returns entries in
|
|
33
|
+
* cursor order. `hasMore: true` means call again with the new cursor.
|
|
34
|
+
* 5. **Compaction (`prune`) may drop superseded intermediate versions but
|
|
35
|
+
* NEVER the latest state or an un-consumed tombstone horizon** — a store
|
|
36
|
+
* advertises its horizon so clients older than it do a full resync.
|
|
37
|
+
*/
|
|
38
|
+
/** What happened to a document. Field-level patches are deliberately out of
|
|
39
|
+
* scope — version-checked upserts + server authority beat CRDT complexity
|
|
40
|
+
* for ERP data (the Sheets/Replicache position, not the Figma one). */
|
|
41
|
+
type ChangeOp = "upsert" | "delete";
|
|
42
|
+
interface ChangeEntry<TDoc = unknown> {
|
|
43
|
+
/** Which logical collection/resource this change belongs to (e.g. `pos-order`). */
|
|
44
|
+
readonly scope: string;
|
|
45
|
+
/** Document identity within the scope. */
|
|
46
|
+
readonly docId: string;
|
|
47
|
+
readonly op: ChangeOp;
|
|
48
|
+
/**
|
|
49
|
+
* Monotonic per-document version — the optimistic-concurrency token.
|
|
50
|
+
* Conflict rule: a push carrying `baseVersion < current` is a conflict.
|
|
51
|
+
*/
|
|
52
|
+
readonly version: number;
|
|
53
|
+
/** Full document snapshot for `upsert`; absent for `delete` (tombstone). */
|
|
54
|
+
readonly doc?: TDoc;
|
|
55
|
+
/** Tenant partition (organizationId) — sync feeds are tenant-scoped. */
|
|
56
|
+
readonly tenantId?: string;
|
|
57
|
+
/** Server clock at capture — informational; ORDERING comes from the cursor. */
|
|
58
|
+
readonly at: Date;
|
|
59
|
+
/** Opaque position of THIS entry in the feed (assigned by the store). */
|
|
60
|
+
readonly cursor: string;
|
|
61
|
+
}
|
|
62
|
+
interface ChangesSinceOptions {
|
|
63
|
+
/** Max entries to return. Stores should default sensibly (e.g. 500). */
|
|
64
|
+
readonly limit?: number;
|
|
65
|
+
/** Restrict to these scopes (a client syncs the resources it opted into). */
|
|
66
|
+
readonly scopes?: readonly string[];
|
|
67
|
+
/** Tenant partition — REQUIRED by multi-tenant stores. */
|
|
68
|
+
readonly tenantId?: string;
|
|
69
|
+
}
|
|
70
|
+
interface ChangesPage<TDoc = unknown> {
|
|
71
|
+
readonly changes: ReadonlyArray<ChangeEntry<TDoc>>;
|
|
72
|
+
/** Checkpoint AFTER applying this page — echo into the next `since`. */
|
|
73
|
+
readonly cursor: string;
|
|
74
|
+
/** True → more entries exist; pull again immediately. */
|
|
75
|
+
readonly hasMore: boolean;
|
|
76
|
+
}
|
|
77
|
+
interface PushMutation<TDoc = unknown> {
|
|
78
|
+
readonly scope: string;
|
|
79
|
+
readonly docId: string;
|
|
80
|
+
readonly op: ChangeOp;
|
|
81
|
+
/** Version the client last saw — the optimistic-concurrency precondition. */
|
|
82
|
+
readonly baseVersion?: number;
|
|
83
|
+
readonly doc?: TDoc;
|
|
84
|
+
/**
|
|
85
|
+
* Client-unique id (`<clientId>:<seq>`) — replays of the same id MUST be
|
|
86
|
+
* acknowledged as already-applied, never re-executed (at-least-once safe).
|
|
87
|
+
*/
|
|
88
|
+
readonly mutationId: string;
|
|
89
|
+
}
|
|
90
|
+
type PushVerdictStatus = "applied" | "already_applied" | "conflict" | "rejected";
|
|
91
|
+
interface PushVerdict<TDoc = unknown> {
|
|
92
|
+
readonly mutationId: string;
|
|
93
|
+
readonly status: PushVerdictStatus;
|
|
94
|
+
/** Authoritative post-push version (also on conflict: the WINNING version). */
|
|
95
|
+
readonly version?: number;
|
|
96
|
+
/** Authoritative doc on conflict so the client can rebase (server wins). */
|
|
97
|
+
readonly current?: TDoc;
|
|
98
|
+
readonly reason?: string;
|
|
99
|
+
}
|
|
100
|
+
interface ChangeLogAppendOptions {
|
|
101
|
+
/** DB session/transaction handle — append atomically with the business write. */
|
|
102
|
+
readonly session?: unknown;
|
|
103
|
+
}
|
|
104
|
+
interface ChangeLogStore<TDoc = unknown> {
|
|
105
|
+
/** Record a change. `cursor`/`at` are ASSIGNED by the store; callers pass the rest. */
|
|
106
|
+
append(entry: Omit<ChangeEntry<TDoc>, "cursor" | "at">, options?: ChangeLogAppendOptions): Promise<ChangeEntry<TDoc>>;
|
|
107
|
+
/** Entries strictly AFTER `cursor` (empty string = from the beginning). */
|
|
108
|
+
since(cursor: string, options?: ChangesSinceOptions): Promise<ChangesPage<TDoc>>;
|
|
109
|
+
/** The current head checkpoint — what a fresh client stores after a full load. */
|
|
110
|
+
latestCursor(options?: Pick<ChangesSinceOptions, "tenantId" | "scopes">): Promise<string>;
|
|
111
|
+
/**
|
|
112
|
+
* Compact entries older than `before`, keeping per-doc latest state.
|
|
113
|
+
* Returns the new HORIZON cursor: clients checkpointed before it must full-resync.
|
|
114
|
+
*/
|
|
115
|
+
prune?(before: Date): Promise<string>;
|
|
116
|
+
}
|
|
117
|
+
/** Client checkpoint older than the store's compaction horizon → full resync. */
|
|
118
|
+
declare class CursorExpiredError extends Error {
|
|
119
|
+
readonly cursor: string;
|
|
120
|
+
readonly horizon: string;
|
|
121
|
+
constructor(cursor: string, horizon: string);
|
|
122
|
+
}
|
|
123
|
+
declare class MemoryChangeLogStore<TDoc = unknown> implements ChangeLogStore<TDoc> {
|
|
124
|
+
private entries;
|
|
125
|
+
private seq;
|
|
126
|
+
append(entry: Omit<ChangeEntry<TDoc>, "cursor" | "at">, _options?: ChangeLogAppendOptions): Promise<ChangeEntry<TDoc>>;
|
|
127
|
+
since(cursor: string, options?: ChangesSinceOptions): Promise<ChangesPage<TDoc>>;
|
|
128
|
+
latestCursor(): Promise<string>;
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
export { ChangeEntry, ChangeLogAppendOptions, ChangeLogStore, ChangeOp, ChangesPage, ChangesSinceOptions, CursorExpiredError, MemoryChangeLogStore, PushMutation, PushVerdict, PushVerdictStatus };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/sync/index.ts
|
|
2
|
+
/** Client checkpoint older than the store's compaction horizon → full resync. */
|
|
3
|
+
var CursorExpiredError = class extends Error {
|
|
4
|
+
constructor(cursor, horizon) {
|
|
5
|
+
super(`[repo-core:sync] cursor "${cursor}" predates the compaction horizon — full resync required.`);
|
|
6
|
+
this.cursor = cursor;
|
|
7
|
+
this.horizon = horizon;
|
|
8
|
+
this.name = "CursorExpiredError";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var MemoryChangeLogStore = class {
|
|
12
|
+
entries = [];
|
|
13
|
+
seq = 0;
|
|
14
|
+
async append(entry, _options) {
|
|
15
|
+
const cursor = String(++this.seq).padStart(16, "0");
|
|
16
|
+
const full = {
|
|
17
|
+
...entry,
|
|
18
|
+
cursor,
|
|
19
|
+
at: /* @__PURE__ */ new Date()
|
|
20
|
+
};
|
|
21
|
+
this.entries.push(full);
|
|
22
|
+
return full;
|
|
23
|
+
}
|
|
24
|
+
async since(cursor, options = {}) {
|
|
25
|
+
const { limit = 500, scopes, tenantId } = options;
|
|
26
|
+
const filtered = this.entries.filter((e) => e.cursor > cursor && (!scopes || scopes.includes(e.scope)) && (tenantId === void 0 || e.tenantId === tenantId));
|
|
27
|
+
const page = filtered.slice(0, limit);
|
|
28
|
+
const last = page[page.length - 1];
|
|
29
|
+
return {
|
|
30
|
+
changes: page,
|
|
31
|
+
cursor: last ? last.cursor : cursor,
|
|
32
|
+
hasMore: filtered.length > page.length
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async latestCursor() {
|
|
36
|
+
const last = this.entries[this.entries.length - 1];
|
|
37
|
+
return last ? last.cursor : "";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
export { CursorExpiredError, MemoryChangeLogStore };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/repo-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Driver-agnostic repository primitives: hooks, Filter IR, operations, pagination, cache contract. Foundation for mongokit, sqlitekit, pgkit, and prismakit. Lean by design — no plugins ship here; each kit owns its own.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -94,7 +94,11 @@
|
|
|
94
94
|
"types": "./dist/lock/index.d.mts",
|
|
95
95
|
"default": "./dist/lock/index.mjs"
|
|
96
96
|
},
|
|
97
|
-
"./package.json": "./package.json"
|
|
97
|
+
"./package.json": "./package.json",
|
|
98
|
+
"./sync": {
|
|
99
|
+
"types": "./dist/sync/index.d.mts",
|
|
100
|
+
"default": "./dist/sync/index.mjs"
|
|
101
|
+
}
|
|
98
102
|
},
|
|
99
103
|
"keywords": [
|
|
100
104
|
"repository",
|