@spooky-sync/core 0.0.1-canary.64 → 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 +56 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +13 -0
- package/package.json +1 -1
- package/src/modules/crdt/index.ts +27 -0
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
|
@@ -3059,6 +3059,7 @@ var CrdtManager = class {
|
|
|
3059
3059
|
* LoroDoc if no CRDT state exists yet (migration path)
|
|
3060
3060
|
*/
|
|
3061
3061
|
async open(table, recordId, field, fallbackText) {
|
|
3062
|
+
this.assertCrdtField(table, field);
|
|
3062
3063
|
const key = this.makeKey(table, recordId, field);
|
|
3063
3064
|
let crdtField = this.fields.get(key);
|
|
3064
3065
|
if (crdtField) return crdtField;
|
|
@@ -3154,6 +3155,18 @@ var CrdtManager = class {
|
|
|
3154
3155
|
makeKey(table, recordId, field) {
|
|
3155
3156
|
return `${table}:${recordId}:${field}`;
|
|
3156
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
|
+
}
|
|
3157
3170
|
};
|
|
3158
3171
|
|
|
3159
3172
|
//#endregion
|
package/package.json
CHANGED
|
@@ -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
|
}
|