@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 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.6.1",
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",