@spooky-sync/core 0.0.1-canary.63 → 0.0.1-canary.65

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/AGENTS.md ADDED
@@ -0,0 +1,56 @@
1
+ # `@spooky-sync/core` — agent guide
2
+
3
+ ## What this package is
4
+
5
+ The sync engine. A `Sp00kyClient<S>` owns a local database (memory or IndexedDB), a remote SurrealDB connection, a mutation queue, and a CRDT manager. It's framework-agnostic — UI bindings live in `@spooky-sync/client-solid` (or future `client-react`, etc.). Most app code touches this package indirectly through a UI binding's `useDb()` hook.
6
+
7
+ ## Mental model
8
+
9
+ ```
10
+ .surql schema
11
+ │ spky generate
12
+
13
+ schema.gen.ts (typed schema + SURQL_SCHEMA constant)
14
+ │ passed to SyncedDbConfig
15
+
16
+ Sp00kyClient<S> ── local store (memory | IndexedDB)
17
+ │ ↑↓ DBSP reactive query layer
18
+ │ ↑↓ mutation queue
19
+ └── SSP ── remote SurrealDB
20
+ ```
21
+
22
+ Local mutations are applied optimistically, ingested into a DBSP layer that drives reactive query updates, then drained to the remote via SSP. Live updates flow back the same path.
23
+
24
+ ## Key exports (`src/index.ts`)
25
+
26
+ - `Sp00kyClient<S>` — main class. Methods: `init()`, `create(id, payload)`, `update(table, id, payload, options?)`, `delete(table, idOrSelector)`, `query(table, opts?)`, `run(backend, route, payload)`, `bucket(name)`, `useRemote(fn)`, `authenticate(token)`, `signOut()`. Plus `pendingMutationCount` and `subscribeToPendingMutations(cb)`.
27
+ - `BucketHandle` — file storage handle (`put`, `get`, `delete`, `exists`).
28
+ - `AuthService` — token management, sign-in/sign-out events.
29
+ - `CrdtManager`, `CrdtField`, `cursorColorFromName`, `CURSOR_COLORS` — Loro-CRDT integration.
30
+ - Types: `Sp00kyConfig`, `SyncedDbConfig` (re-exported by client-solid as the consumer-facing shape), `QueryTimeToLive`, `PersistenceClient`, `StoreType`, `UpdateOptions`, `RunOptions`.
31
+ - Subpath: `@spooky-sync/core/otel` — `createOtelTransmit(endpoint)` for piping pino logs to OpenTelemetry.
32
+
33
+ ## Common gotchas
34
+
35
+ - **Use UI hooks, not the client directly.** In a Solid app, call `useDb()` from `@spooky-sync/client-solid`. Touch `Sp00kyClient` only inside `provider.client` for advanced flows.
36
+ - **Mutations are optimistic.** `db.create` / `db.update` / `db.delete` return immediately; the queue drains in the background. Inspect progress via `pendingMutationCount`.
37
+ - **Record IDs are full strings.** `db.create('thread:abc123', {...})` — *not* `db.create('thread', { id: 'abc123' })`. The first arg is `'<table>:<id>'`. Generate IDs with `Uuid` from `surrealdb` (re-exported by `client-solid`).
38
+ - **`db.update` takes `(table, id, payload, options?)`**. The `debounced` option (`{ debounced: true }` or `{ debounced: { delay, key } }`) coalesces rapid updates — use it for CRDT text/title fields.
39
+ - **`db.useRemote(fn)` bypasses the cache.** Use only for queries that can't be expressed via the query builder (e.g. graph traversals across `RELATE` edges). Results are not synced into the local cache.
40
+ - **`@parent` columns are auto-populated.** Defined in the `.surql` schema as `record<user>` with the `-- @parent` annotation; never write them yourself — they're set from the auth context server-side.
41
+
42
+ ## Module map (under `src/modules/`)
43
+
44
+ - `data/` — local store + DBSP reactive query layer.
45
+ - `sync/` — SSP client, mutation queue, live-update ingestion.
46
+ - `cache/` — query result cache with TTL.
47
+ - `crdt/` — Loro CRDT manager for collaborative text fields.
48
+ - `auth/` — token storage, sign-in flow.
49
+ - `devtools/` — devtools bridge (talks to the browser extension and `@spooky-sync/devtools-mcp`).
50
+
51
+ ## Pointers
52
+
53
+ - UI bindings: `node_modules/@spooky-sync/client-solid/AGENTS.md`
54
+ - Query builder DSL: `node_modules/@spooky-sync/query-builder/AGENTS.md`
55
+ - Schema codegen / migrations: `node_modules/@spooky-sync/cli/AGENTS.md`
56
+ - Live introspection: `node_modules/@spooky-sync/devtools-mcp/AGENTS.md`
package/dist/index.d.ts CHANGED
@@ -261,6 +261,12 @@ declare class CrdtManager {
261
261
  private ensureLiveSelect;
262
262
  private killLiveSelect;
263
263
  private makeKey;
264
+ /**
265
+ * Throws if `<table>.<field>` is not annotated `@crdt` in the schema. Catches
266
+ * typos, removed annotations, and stale schema codegen at the call site instead
267
+ * of silently producing a non-CRDT writer.
268
+ */
269
+ private assertCrdtField;
264
270
  }
265
271
  //#endregion
266
272
  //#region src/sp00ky.d.ts
package/dist/index.js CHANGED
@@ -1809,7 +1809,6 @@ var Sp00kySync = class {
1809
1809
  this.isInit = true;
1810
1810
  await this.scheduler.init();
1811
1811
  this.scheduler.syncUp();
1812
- this.scheduler.syncUp();
1813
1812
  this.scheduler.syncDown();
1814
1813
  this.startRefLiveQueries();
1815
1814
  }
@@ -3060,6 +3059,7 @@ var CrdtManager = class {
3060
3059
  * LoroDoc if no CRDT state exists yet (migration path)
3061
3060
  */
3062
3061
  async open(table, recordId, field, fallbackText) {
3062
+ this.assertCrdtField(table, field);
3063
3063
  const key = this.makeKey(table, recordId, field);
3064
3064
  let crdtField = this.fields.get(key);
3065
3065
  if (crdtField) return crdtField;
@@ -3155,6 +3155,18 @@ var CrdtManager = class {
3155
3155
  makeKey(table, recordId, field) {
3156
3156
  return `${table}:${recordId}:${field}`;
3157
3157
  }
3158
+ /**
3159
+ * Throws if `<table>.<field>` is not annotated `@crdt` in the schema. Catches
3160
+ * typos, removed annotations, and stale schema codegen at the call site instead
3161
+ * of silently producing a non-CRDT writer.
3162
+ */
3163
+ assertCrdtField(table, field) {
3164
+ const tableSchema = this.schema.tables.find((t) => t.name === table);
3165
+ if (!tableSchema) throw new Error(`openCrdtField: unknown table '${table}'. Available: ${this.schema.tables.map((t) => t.name).join(", ")}`);
3166
+ const column = tableSchema.columns[field];
3167
+ if (!column) throw new Error(`openCrdtField: '${table}.${field}' is not in the schema. Available fields: ${Object.keys(tableSchema.columns).join(", ")}`);
3168
+ if (!column.crdt) throw new Error(`openCrdtField: '${table}.${field}' is not annotated '@crdt' in the schema. Add '-- @crdt text' above the field's DEFINE FIELD and regenerate the client schema.`);
3169
+ }
3158
3170
  };
3159
3171
 
3160
3172
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spooky-sync/core",
3
- "version": "0.0.1-canary.63",
3
+ "version": "0.0.1-canary.65",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -41,6 +41,7 @@ export class CrdtManager {
41
41
  field: string,
42
42
  fallbackText?: string,
43
43
  ): Promise<CrdtField> {
44
+ this.assertCrdtField(table, field);
44
45
  const key = this.makeKey(table, recordId, field);
45
46
  let crdtField = this.fields.get(key);
46
47
 
@@ -175,4 +176,30 @@ export class CrdtManager {
175
176
  private makeKey(table: string, recordId: string, field: string): string {
176
177
  return `${table}:${recordId}:${field}`;
177
178
  }
179
+
180
+ /**
181
+ * Throws if `<table>.<field>` is not annotated `@crdt` in the schema. Catches
182
+ * typos, removed annotations, and stale schema codegen at the call site instead
183
+ * of silently producing a non-CRDT writer.
184
+ */
185
+ private assertCrdtField(table: string, field: string): void {
186
+ const tableSchema = this.schema.tables.find((t) => t.name === table);
187
+ if (!tableSchema) {
188
+ throw new Error(
189
+ `openCrdtField: unknown table '${table}'. Available: ${this.schema.tables.map((t) => t.name).join(', ')}`
190
+ );
191
+ }
192
+ const column = tableSchema.columns[field];
193
+ if (!column) {
194
+ throw new Error(
195
+ `openCrdtField: '${table}.${field}' is not in the schema. Available fields: ${Object.keys(tableSchema.columns).join(', ')}`
196
+ );
197
+ }
198
+ if (!column.crdt) {
199
+ throw new Error(
200
+ `openCrdtField: '${table}.${field}' is not annotated '@crdt' in the schema. ` +
201
+ `Add '-- @crdt text' above the field's DEFINE FIELD and regenerate the client schema.`
202
+ );
203
+ }
204
+ }
178
205
  }
@@ -86,7 +86,6 @@ export class Sp00kySync<S extends SchemaStructure> {
86
86
  this.isInit = true;
87
87
  await this.scheduler.init();
88
88
  void this.scheduler.syncUp();
89
- void this.scheduler.syncUp();
90
89
  void this.scheduler.syncDown();
91
90
  void this.startRefLiveQueries();
92
91
  }