@spooky-sync/client-solid2 0.0.1-alpha.1
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 +81 -0
- package/QUICK_START.md +129 -0
- package/README.md +19 -0
- package/dist/index.cjs +1290 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +670 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +670 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1266 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
- package/skills/sp00ky-solid2/SKILL.md +71 -0
- package/src/index.ts +410 -0
- package/src/lib/Blurhash.ts +64 -0
- package/src/lib/BucketImage.ts +170 -0
- package/src/lib/Sp00kyProvider.ts +104 -0
- package/src/lib/__tests__/conflate.test.ts +120 -0
- package/src/lib/__tests__/create-query.test.ts +397 -0
- package/src/lib/__tests__/rc-semantics.test.ts +393 -0
- package/src/lib/__tests__/use-sync-activity.test.ts +104 -0
- package/src/lib/conflate.ts +74 -0
- package/src/lib/context.ts +28 -0
- package/src/lib/create-preload.ts +116 -0
- package/src/lib/create-query.ts +388 -0
- package/src/lib/create-submission.ts +57 -0
- package/src/lib/from-subscription.ts +32 -0
- package/src/lib/merge-rows.test.ts +92 -0
- package/src/lib/merge-rows.ts +72 -0
- package/src/lib/models.ts +8 -0
- package/src/lib/use-app-release.ts +89 -0
- package/src/lib/use-blurhash.ts +82 -0
- package/src/lib/use-bucket-image.ts +108 -0
- package/src/lib/use-crdt-field.ts +57 -0
- package/src/lib/use-download-file.ts +181 -0
- package/src/lib/use-feature-flag.ts +43 -0
- package/src/lib/use-file-upload.ts +146 -0
- package/src/lib/use-storage-status.ts +44 -0
- package/src/lib/use-sync-activity.ts +99 -0
- package/src/lib/use-sync-status.ts +63 -0
- package/src/types/index.ts +83 -0
- package/tsconfig.json +27 -0
- package/tsdown.config.ts +20 -0
- package/vitest.config.ts +14 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# `@spooky-sync/client-solid2` — agent guide
|
|
2
|
+
|
|
3
|
+
## What this package is
|
|
4
|
+
|
|
5
|
+
The **Solid 2.0** binding for sp00ky. Counterpart of `@spooky-sync/client-solid` (Solid 1.x); the two coexist until Solid 2.0 is stable and an app uses exactly one. Peers: `solid-js@^2.0.0-rc.0` + `@solidjs/signals@^2.0.0-rc.0` (coordinated RC, pin matching versions); apps also need `@solidjs/web` and `jsxImportSource: "@solidjs/web"`.
|
|
6
|
+
|
|
7
|
+
Solid-2 native internals: query results live in a `createProjection` fed by an async generator over the engine's live subscription (keyed reconcile by `id`, row identity preserved, `<For>` notified on add/remove/reorder — no version-signal hack). Status hooks are async-iterable-backed memos with a `loadingValue`. Teardown is `onCleanup`-driven, because Solid 2 abandons superseded async generators without terminating them.
|
|
8
|
+
|
|
9
|
+
## Setup pattern
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// db.ts
|
|
13
|
+
import type { SyncedDbConfig } from '@spooky-sync/client-solid2';
|
|
14
|
+
import { schema, SURQL_SCHEMA } from './schema.gen'; // generated by `spky generate`
|
|
15
|
+
|
|
16
|
+
export const dbConfig: SyncedDbConfig<typeof schema> = {
|
|
17
|
+
schema,
|
|
18
|
+
schemaSurql: SURQL_SCHEMA,
|
|
19
|
+
database: {
|
|
20
|
+
namespace: 'main',
|
|
21
|
+
database: 'app',
|
|
22
|
+
endpoint: 'ws://localhost:8666/rpc',
|
|
23
|
+
store: 'indexeddb',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
// App.tsx
|
|
30
|
+
<Sp00kyProvider config={dbConfig} fallback={<Splash />}>{/* app */}</Sp00kyProvider>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Provider props are identical to client-solid: `config`, `fallback`, `preload` (awaited gate before children render), `onReady`, `onError`. A mounted client is deliberately not closed on unmount.
|
|
34
|
+
|
|
35
|
+
## Key API
|
|
36
|
+
|
|
37
|
+
- **`useDb<typeof schema>()`** — the `SyncedDb<S>` instance. Same surface as client-solid: `create`, `update`, `delete`, `query`, `preload`, `run`, `bucket`, `remoteQuery`, `useRemote`, `auth`/`signOut`, `pendingMutationCount`, `subscribeToPendingMutations`, `syncHealth`/`storageHealth` + their subscribe methods.
|
|
38
|
+
- **`createQuery(query, options?)`** (or `createQuery(db, query, options?)`) — reactive query. `useQuery` is a deprecated alias. Pass a thunk when the query depends on signals; read the signals inside it.
|
|
39
|
+
- `data()` — non-suspending. `[]` / `null` before the first result, keyed-reconciled live rows after.
|
|
40
|
+
- `ready()` — suspending read for `<Loading fallback={...}>` (Solid 2's renamed `<Suspense>`).
|
|
41
|
+
- `error()` — registration/sync failure (e.g. SSP 503 NOT_READY). Never thrown into the render tree; the sync scheduler retries underneath.
|
|
42
|
+
- `isLoading()` — cold first load only (nothing painted for this identity, server not heard, no error). A cached paint ends it; an authoritative empty answer ends it. Never true again for the same identity.
|
|
43
|
+
- `isFetching()` — a fetch cycle is in flight (registration, a sync round).
|
|
44
|
+
- `isAuthoritative()` — server membership is known for this identity (latched until it changes). `hasData()` — `data()` holds something. `isEmpty()` — authoritative AND no data: the server said "no rows"; render empty states on this, not on `!hasData()`.
|
|
45
|
+
- `isSettled()` — authoritative AND idle; gate windowed-list end detection on it. Not monotonic; latch it (a `createMemo(prev => prev || q.isSettled())`) for a verdict that must stick.
|
|
46
|
+
- Options: `enabled?: () => boolean`, `deregisterOnCleanup?: boolean`.
|
|
47
|
+
- **`createSubmission(fn)`** — button pending/error state around a mutation: `submit()`, `pending()`, `error()`, `result()`, `clearError()`. There is deliberately no `action()` / `createOptimisticStore` layer (see `src/lib/create-submission.ts`).
|
|
48
|
+
- **`useSyncActivity(options?)`** — `{ fetchingQueries, pendingMutations, isDownloading, isUploading }`. One subscription on the engine's aggregate fetch count plus the outbox depth; `isDownloading()` is debounced ON by `downloadDelayMs` (200) and off at once, `isUploading()` is `pendingMutations() > uploadThreshold` (1). Drive a logo/traffic indicator off it; pair with `useSyncStatus().isOffline()` to show the unsaved count while disconnected.
|
|
49
|
+
- **`createPreload`**, **`usePendingMutations`**, **`useSyncStatus`**, **`useStorageStatus`**, **`useFeatureFlag`**, **`useAppRelease`**, **`useCrdtField`**, **`useFileUpload`**, **`useDownloadFile`** — same shapes as client-solid.
|
|
50
|
+
- **`conflate`**, **`fromSubscription`** — the async-iterable helpers the hooks are built on; useful for wrapping other engine subscriptions.
|
|
51
|
+
|
|
52
|
+
## Mutations
|
|
53
|
+
|
|
54
|
+
The engine is optimistic local-first end to end (local commit → live queries re-emit → outbox sync; `db.run()` writes an outbox job row with `status: 'pending'`). So mutations are plain awaited calls, no optimistic-update layer on top:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
await db.create('post:' + crypto.randomUUID(), { title: 'Hi' });
|
|
58
|
+
await db.update('post', 'post:xyz', { title: 'Edited' });
|
|
59
|
+
await db.delete('post', row.id);
|
|
60
|
+
await db.run('backend', 'sendMail', { to });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Track a backend job by querying its outbox row: `createQuery(() => db.query('job_outbox').where({ id: jobId() }).one().build())`.
|
|
64
|
+
|
|
65
|
+
## Common gotchas
|
|
66
|
+
|
|
67
|
+
- **Rows are store proxies.** Solid 2 stores wrap class instances too and serve their methods **bound**, so a `RecordId` read out of a row reports `constructor.name === 'bound RecordId'`. `db.delete('post', row.id)` handles that; for surrealdb APIs that check `instanceof`, unwrap first with `snapshot(row)` from `solid-js`.
|
|
68
|
+
- **`<Loading>`, not `<Suspense>`.** Only `ready()` suspends; `data()` never does, which is what keeps local-first cache paints instant.
|
|
69
|
+
- **`data()` is never `undefined`.** It is born `[]` / `null` (`seedLoadingValue`), unlike client-solid. Branch on `isLoading()`, not on nullish data.
|
|
70
|
+
- **Coordinated RC.** `solid-js`, `@solidjs/web`, `@solidjs/signals` must be on matching `2.0.0-rc.x` versions. After any Solid bump, run `src/lib/__tests__/rc-semantics.test.ts` — it probes the RC contracts this package depends on.
|
|
71
|
+
- **Vitest resolves the SSR build.** Under Node, `solid-js`'s `node` export condition gives you the SSR build where user effects never run. Set `resolve.conditions: ['browser', 'development']` (see this package's `vitest.config.ts`).
|
|
72
|
+
- **`createQuery` factories must call `.build()`** (or `.all()`, `.one()`, …). A bare `db.query('post')` is a builder, not a query.
|
|
73
|
+
- **Provider is mandatory.** Any hook outside `<Sp00kyProvider>` throws.
|
|
74
|
+
|
|
75
|
+
## Pointers
|
|
76
|
+
|
|
77
|
+
- Usage walkthrough + migration table: `QUICK_START.md` (same content as the docs' [Solid 2 guide](https://mono424.github.io/sp00ky/docs/guide/solid2))
|
|
78
|
+
- Skill for coding agents: `skills/sp00ky-solid2/SKILL.md`
|
|
79
|
+
- Solid 1.x binding: `node_modules/@spooky-sync/client-solid/AGENTS.md`
|
|
80
|
+
- Sync engine: `node_modules/@spooky-sync/core/AGENTS.md`
|
|
81
|
+
- Query builder DSL: `node_modules/@spooky-sync/query-builder/AGENTS.md`
|
package/QUICK_START.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# @spooky-sync/client-solid2 — Quick Start
|
|
2
|
+
|
|
3
|
+
Solid **2.0** native bindings for the Sp00ky reactive local-first SurrealDB framework. This is the Solid 2 counterpart of `@spooky-sync/client-solid` (which stays on Solid 1.x); the two packages coexist until Solid 2.0 is stable and apps migrate.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @spooky-sync/client-solid2 solid-js@2.0.0-rc.0 @solidjs/web@2.0.0-rc.0 @solidjs/signals@2.0.0-rc.0
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> **Coordinated RC**: `solid-js`, `@solidjs/web`, and `@solidjs/signals` must be on matching `2.0.0-rc.x` versions. Pin exact versions; this package's semantics probes (`rc-semantics.test.ts`) catch drift on bumps. `jsxImportSource` is `@solidjs/web` in Solid 2.
|
|
12
|
+
|
|
13
|
+
## Provider
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Sp00kyProvider } from '@spooky-sync/client-solid2';
|
|
17
|
+
import { schema } from './generated/schema';
|
|
18
|
+
import schemaSurql from './generated/schema.surql?raw';
|
|
19
|
+
|
|
20
|
+
function App() {
|
|
21
|
+
return (
|
|
22
|
+
<Sp00kyProvider
|
|
23
|
+
config={{
|
|
24
|
+
database: { endpoint: 'ws://localhost:8000', namespace: 'my_ns', database: 'my_db', store: 'indexeddb' },
|
|
25
|
+
schema,
|
|
26
|
+
schemaSurql,
|
|
27
|
+
}}
|
|
28
|
+
fallback={<div>Loading database…</div>}
|
|
29
|
+
preload={async (db) => { await db.preload(db.query('config').build()); }}
|
|
30
|
+
>
|
|
31
|
+
<MyApp />
|
|
32
|
+
</Sp00kyProvider>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Same props as client-solid's provider (`config`, `fallback`, `preload`, `onReady`, `onError`). A live mounted client is deliberately not closed on unmount (see source comment).
|
|
38
|
+
|
|
39
|
+
## createQuery
|
|
40
|
+
|
|
41
|
+
`createQuery` replaces `useQuery` (which remains as a deprecated alias). Same overloads: `(query, options?)` with context, or `(db, query, options?)`.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { createQuery, useDb } from '@spooky-sync/client-solid2';
|
|
45
|
+
|
|
46
|
+
function PostList() {
|
|
47
|
+
const db = useDb();
|
|
48
|
+
const posts = createQuery(db.query('post').orderBy('createdAt', 'desc').limit(20).build());
|
|
49
|
+
|
|
50
|
+
// Accessor style — data() never suspends, born as [] and live thereafter:
|
|
51
|
+
return (
|
|
52
|
+
<Show when={!posts.isLoading()} fallback={<div>Loading…</div>}>
|
|
53
|
+
<For each={posts.data()}>{(post) => <PostRow post={post} />}</For>
|
|
54
|
+
</Show>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Reactive inputs: pass a thunk, read your signals inside it.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
const post = createQuery(() => (postId() ? db.query('post').where({ id: postId() }).one().build() : null));
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Suspension style (`<Loading>`)
|
|
66
|
+
|
|
67
|
+
`q.ready()` reads the same data but participates in Solid 2's boundary protocol: it pends until the first real result.
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { Loading } from 'solid-js';
|
|
71
|
+
|
|
72
|
+
<Loading fallback={<Skeleton />}>
|
|
73
|
+
<For each={posts.ready()}>{(post) => <PostRow post={post} />}</For>
|
|
74
|
+
</Loading>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Result surface
|
|
78
|
+
|
|
79
|
+
| Accessor | Meaning |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `data()` | Rows (or row/`null` for `.one()`). Never suspends, never throws. Keyed-reconciled in place: unchanged rows keep identity, `<For>` is notified on add/remove/reorder. |
|
|
82
|
+
| `ready()` | Same data, suspends into the nearest `<Loading>` until first result or error. |
|
|
83
|
+
| `error()` | Registration/sync error (e.g. SSP 503 during bootstrap). Never thrown into the render tree; the sync scheduler retries underneath. |
|
|
84
|
+
| `isLoading()` | Cold first load only: nothing painted for this identity yet, server not heard, no error. A cached paint ends it; so does the server answering with zero rows. |
|
|
85
|
+
| `isFetching()` | Sync engine is pulling records for this query. |
|
|
86
|
+
| `isAuthoritative()` | Server membership is known for this identity (registration/poll landed, or a previous session's membership was read on boot). Latched until the identity changes. |
|
|
87
|
+
| `hasData()` | `data()` holds rows (or the `.one()` row). |
|
|
88
|
+
| `isEmpty()` | Authoritative AND no data — the server said "no rows". Render empty states on this. |
|
|
89
|
+
| `isSettled()` | Authoritative AND idle — windowed lists may trust a short result as the true end of the list. Re-enters on later sync rounds; latch it for a sticky verdict. |
|
|
90
|
+
|
|
91
|
+
Options: `{ enabled?: () => boolean, deregisterOnCleanup?: boolean }` — both as in client-solid.
|
|
92
|
+
|
|
93
|
+
### Gotcha: rows are store proxies
|
|
94
|
+
|
|
95
|
+
Solid 2 stores wrap class instances too, and serve methods bound. A `RecordId` read out of a row works for display and for `db.delete('table', row.id)`, but if you pass whole row objects back into surrealdb APIs that check `instanceof`, unwrap first with `snapshot(row)` (exported by `solid-js`).
|
|
96
|
+
|
|
97
|
+
## Mutations
|
|
98
|
+
|
|
99
|
+
Engine writes are already optimistic local-first (local commit → live queries re-emit → outbox sync; `db.run()` is itself an outbox job row with `status: 'pending'`). Plain calls:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
await db.create('post:xyz', { title: 'Hi' });
|
|
103
|
+
await db.update('post', 'post:xyz', { title: 'Edited' });
|
|
104
|
+
await db.delete('post', row.id);
|
|
105
|
+
await db.run('backend', 'sendMail', { to });
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
For button pending/error state, wrap with `createSubmission`:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
const save = createSubmission((title: string) => db.create(`post:${crypto.randomUUID()}`, { title }));
|
|
112
|
+
<button disabled={save.pending()} onClick={() => save.submit(title())}>Save</button>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Track a backend job's progress by querying its outbox row: `createQuery(() => db.query('job_outbox').where({ id: jobId() }).one().build())`.
|
|
116
|
+
|
|
117
|
+
## Status hooks
|
|
118
|
+
|
|
119
|
+
Same shapes as client-solid, rebuilt on async-iterable-backed memos: `useSyncStatus`, `useStorageStatus`, `usePendingMutations`, `useSyncActivity` (new: `isDownloading()` after 200 ms of fetching, `isUploading()` above one queued write, for a traffic indicator), `useFeatureFlag`, `useAppRelease`, `useCrdtField`, `useFileUpload`, `useDownloadFile`, `createPreload`.
|
|
120
|
+
|
|
121
|
+
## Migrating from client-solid
|
|
122
|
+
|
|
123
|
+
| client-solid (Solid 1) | client-solid2 (Solid 2) |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `useQuery(q)` | `createQuery(q)` (alias `useQuery` kept) |
|
|
126
|
+
| `data()` may be `undefined` pre-fetch | `data()` is `[]` / `null` pre-fetch |
|
|
127
|
+
| No suspension | `ready()` + `<Loading>` |
|
|
128
|
+
| `useDb()` throws custom error | same API (Solid 2 context throws on missing provider) |
|
|
129
|
+
| everything else | unchanged call signatures |
|
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @spooky-sync/client-solid2
|
|
2
|
+
|
|
3
|
+
Solid **2.0** native bindings for the Sp00ky reactive local-first SurrealDB framework.
|
|
4
|
+
|
|
5
|
+
Counterpart of `@spooky-sync/client-solid` (Solid 1.x); the two coexist until Solid 2.0 is stable. Peer deps: `solid-js@^2.0.0-rc.0` + `@solidjs/signals@^2.0.0-rc.0` (coordinated RC — pin matching versions, apps also need `@solidjs/web`).
|
|
6
|
+
|
|
7
|
+
What "native" means here:
|
|
8
|
+
|
|
9
|
+
- Query results are a plain store written from the engine's live subscription and merged keyed by `id`: row identity preserved, coarse `<For>` readers notified, only changed fields written. Not an async-generator projection - see `create-query.ts` for why a never-returning generator breaks navigation transitions.
|
|
10
|
+
- `createQuery` exposes both worlds: non-suspending accessors (`data`, `isLoading`, `isFetching`, `isAuthoritative`, `hasData`, `isEmpty`, `isSettled`, `error`) and a suspending `ready()` for `<Loading>` boundaries. Born committed, so local-first cache paints never suspend. `isLoading()` is true only on a cold first load (nothing cached, server not heard yet); `isEmpty()` is the server saying "no rows".
|
|
11
|
+
- Status hooks (`useSyncStatus`, `useSyncActivity`, `useStorageStatus`, `usePendingMutations`, feature flags, app release) are async-iterable-backed memos with `loadingValue`. `useSyncActivity` is the traffic indicator pair: `isDownloading()` (queries fetching for longer than 200 ms) and `isUploading()` (more than one write queued), plus the raw `fetchingQueries()` / `pendingMutations()` counts.
|
|
12
|
+
- Mutations stay plain async calls — the engine is already optimistic local-first end to end (local commit → live re-emit → outbox sync). `createSubmission` adds button pending/error state.
|
|
13
|
+
|
|
14
|
+
See `QUICK_START.md` for usage and the migration table from client-solid, and `src/lib/__tests__/rc-semantics.test.ts` for the probed Solid 2 rc contracts this package depends on (run them after every Solid version bump).
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm --filter @spooky-sync/client-solid2 test
|
|
18
|
+
pnpm --filter @spooky-sync/client-solid2 build
|
|
19
|
+
```
|