spacewave 0.57.7 → 0.57.9

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/API.md CHANGED
@@ -3,7 +3,8 @@
3
3
  Spacewave provides typed live collections backed by durable storage. An
4
4
  application declares a schema of collections and mutations. Clients hold one
5
5
  WebSocket connection and read full snapshots that update live. A Node server
6
- owns storage, authentication, and per-operation authorization.
6
+ stores the data, authenticates each connection, and checks access on every
7
+ operation.
7
8
 
8
9
  Package entries:
9
10
 
@@ -51,6 +52,102 @@ The server validates each write against the validator and stores the
51
52
  validator's output form. Reads and watch snapshots return that stored form, so
52
53
  client types use `Output<V>`.
53
54
 
55
+ ## Shared application definitions
56
+
57
+ `defineApp(schema, handlers)` bundles a schema with the code for its named
58
+ mutations. It does not open storage, so the same definition can serve a Node
59
+ server and a Space plugin. Spread it into `createServer`:
60
+
61
+ ```ts
62
+ import { defineApp } from 'spacewave'
63
+
64
+ export const app = defineApp(schema, {
65
+ async completeTodo({ collection }, { id }) {
66
+ const todos = collection('todos')
67
+ const todo = await todos.get(id)
68
+ if (!todo) throw new Error('Todo is unavailable')
69
+ const completed = { ...todo, done: true }
70
+ await todos.put(id, completed)
71
+ return completed
72
+ },
73
+ })
74
+
75
+ const server = await createServer({
76
+ ...app,
77
+ directory: './data',
78
+ authenticate,
79
+ authorize,
80
+ })
81
+ ```
82
+
83
+ The optional `instance` setting in the server configuration picks a dataset
84
+ within the World. It defaults to `schema.id`. Each app instance and authorized
85
+ scope has its own collection records and its own record of accepted requests.
86
+
87
+ The Space plugin SDK reads the same definition through `definePlugin` and
88
+ `attachApp`. An attachment implements `AppSource`, which offers read-only
89
+ collections, named mutations, and function queries. Mutations run inside the
90
+ World transaction that the host provides. A mutation must be deterministic: it
91
+ may use only its input and that transaction, and it must await every
92
+ collection call before it returns.
93
+
94
+ Plugin code is trusted. Transaction and data limits and cooperative
95
+ cancellation bound its work, but they are not a sandbox.
96
+
97
+ ### Function queries in a Space or Node host
98
+
99
+ A function query is a JavaScript function that reads collections and returns a
100
+ value. Declare one with `defineQuery(schema, { name, input, evaluate })`:
101
+
102
+ ```ts
103
+ import { defineQuery } from 'spacewave'
104
+
105
+ const incomplete = defineQuery(schema, {
106
+ name: 'incomplete-todos',
107
+ input: z.null(),
108
+ async evaluate({ collection }) {
109
+ return (await collection('todos').scan())
110
+ .filter(({ value }) => !value.done)
111
+ },
112
+ })
113
+
114
+ for await (const snapshot of attachment.watch(incomplete, null, signal)) {
115
+ if (snapshot.status === 'current') render(snapshot.value)
116
+ }
117
+ ```
118
+
119
+ While the function runs, the runtime records what it read: the records it
120
+ looked up, the records it found missing, the prefixes it scanned, and the
121
+ fields it used. Each result comes from one World snapshot. When the World
122
+ changes, the runtime compares storage roots to skip unchanged data, then checks
123
+ whether any changed record affects what the query read. Only then does it run
124
+ the query again. If the old roots are no longer available, it runs the query
125
+ again in full.
126
+
127
+ A query function must await every read before it returns. It must not change
128
+ its input or the records it reads, cause outside effects, or depend on the
129
+ clock or random numbers. A query that returns a whole record depends on every
130
+ field of that record.
131
+
132
+ Identical queries on the same authorized attachment share one evaluation. It
133
+ stops when the last subscriber closes.
134
+
135
+ On a Node host, `server.as(principal)` implements the same `AppSource`. Call its
136
+ `watch` method with a query definition, or pass it to the React hooks below.
137
+ Each delivery checks collection permissions again. The scoped object starts no
138
+ query work until you read from its iterator. Closing the last iterator releases
139
+ the source, and closing the server stops every active source.
140
+
141
+ `spacewave/react` exports `useAppQuery(source, query, args)` and
142
+ `useAppMutation(source, name)`. A query reports pending, current, or error. A
143
+ mutation reports idle, pending, accepted, or error, and provides
144
+ `submit(input)` and `retry()`. `retry()` resends the original input with the
145
+ original request ID, so the server applies it at most once. Keep personal
146
+ selection and unsaved drafts in component state.
147
+
148
+ The WebSocket `Database` client below subscribes to collection prefixes. It
149
+ does not run function queries.
150
+
54
151
  ## Client
55
152
 
56
153
  ```ts
@@ -64,8 +161,8 @@ const db = await connect({
64
161
  ```
65
162
 
66
163
  `connect(options)` returns `Promise<Database<S>>`. It resolves after
67
- authentication, the wire and schema version check, and root admission. On
68
- failure it closes the connection and throws `SyncError`.
164
+ authentication, the wire and schema version check, and the server's acceptance
165
+ of the connection. On failure it closes the connection and throws `SyncError`.
69
166
 
70
167
  `ConnectOptions<S>`:
71
168
 
@@ -87,8 +184,8 @@ failure it closes the connection and throws `SyncError`.
87
184
  | `close()` | Idempotent. Also available through `await using` (`AsyncDisposable`). |
88
185
 
89
186
  `ConnectionState` is `{ status: 'ready' | 'reconnecting' | 'closed', error? }`.
90
- A new client is `reconnecting` until admission completes, and `closed` after
91
- `close()`.
187
+ A new client is `reconnecting` until the server accepts the connection, and
188
+ `closed` after `close()`.
92
189
 
93
190
  One collection exposes:
94
191
 
@@ -123,7 +220,7 @@ array of `{ key, value }` entries:
123
220
  Each state carries the full snapshot of every record matching the prefix,
124
221
  ordered by key. There are no per-row deltas and no per-row filters. The client
125
222
  reconnects on its own: a lost connection publishes `stale` and the stream
126
- resumes with `current` once the server admits the client again. An
223
+ resumes with `current` once the server accepts the connection again. An
127
224
  unrecoverable failure publishes `error` and ends the stream.
128
225
 
129
226
  ```ts
@@ -197,7 +294,7 @@ at compile time. The schema argument carries no runtime state.
197
294
 
198
295
  | Binding | Contract |
199
296
  | --- | --- |
200
- | `SyncProvider({ db, children })` | Supplies an already-connected `Database<S>`. It never opens, retries, or closes the db; the application owns that lifetime. |
297
+ | `SyncProvider({ db, children })` | Supplies an already-connected `Database<S>`. It never opens, retries, or closes the db; the application does that. |
201
298
  | `useDatabase()` | Returns the typed `Database<S>`. Throws when no provider is above in the tree. |
202
299
  | `useCollection(name, query?)` | Returns `SubscriptionState<Output>` for the collection. |
203
300
 
@@ -256,7 +353,7 @@ const server = await createServer({
256
353
 
257
354
  `createServer(options)` returns `Promise<SyncServer<S, P>>`. It opens the
258
355
  dataset, checks the stored application ID and version, runs any migration, and
259
- only then returns a server that can admit callers. Failures throw `SyncError`.
356
+ only then returns a server that can accept callers. Failures throw `SyncError`.
260
357
 
261
358
  `ServerOptions<S, P>`:
262
359
 
@@ -268,7 +365,7 @@ only then returns a server that can admit callers. Failures throw `SyncError`.
268
365
  | `authorize(access)` | Returns `boolean \| Promise<boolean>`. Called for every operation and again for every watch delivery. |
269
366
  | `mutations` | One handler per declared mutation. |
270
367
  | `limits?` | `maxRecords` (default 10000), `maxSnapshotBytes` (default 8 MiB), `maxRecordBytes` (default 256 KiB). Exceeding a bound fails that query or write with `QUERY_LIMIT`. |
271
- | `migration?` | Runs before any caller is admitted when the stored schema version differs. |
368
+ | `migration?` | Runs before the server accepts any caller when the stored schema version differs. |
272
369
 
273
370
  Storage uses built-in SQLite in a dedicated worker. A directory permits one open server writer at a time.
274
371
 
@@ -306,8 +403,8 @@ migration: {
306
403
 
307
404
  `Migration<S>` is `{ from, run(context) }`. `run` receives `scopes()` listing
308
405
  the stored scopes, `scope(name)` returning a transaction for that scope, and
309
- `signal`. The migration runs inside one maintenance transaction before any
310
- caller is admitted. If the stored version differs and no matching migration is
406
+ `signal`. The migration runs inside one maintenance transaction before the
407
+ server accepts any caller. If the stored version differs and no matching migration is
311
408
  supplied, or the dataset belongs to a different application ID, `createServer`
312
409
  fails with `SCHEMA_MISMATCH`.
313
410
 
@@ -316,8 +413,8 @@ fails with `SCHEMA_MISMATCH`.
316
413
  | Member | Contract |
317
414
  | --- | --- |
318
415
  | `listen(options?)` | Opens its own HTTP server. `ListenerOptions` adds `port?` (default 8787) and `host?` (default 127.0.0.1). Returns `Listener` with a `ws://` `url` and `close()`. |
319
- | `attach(http, options?)` | Handles the upgrade event of a supplied HTTP server. Other routes and upgrade paths stay yours. Returns `Attachment` with `close()`. |
320
- | `as(principal)` | `DatabaseAccess<S>` for direct server-side access as that principal. Every operation still passes `authorize`. |
416
+ | `attach(http, options?)` | Handles the upgrade event of a supplied HTTP server. Your server keeps handling other routes and upgrade paths. Returns `Attachment` with `close()`. |
417
+ | `as(principal)` | `DatabaseAccess<S> & AppSource<S>` for server-side reads, named mutations, and function queries as that principal. Every operation still passes `authorize`. |
321
418
  | `admin(scope)` | Privileged access as subject `admin`, with only the base principal fields. Handlers that require application-specific principal fields should use `as(principal)`. Collection authorization is bypassed; validation still applies. |
322
419
  | `close()` | Closes every attachment the server created, then the application, then storage the server opened. Idempotent. Also available through `await using`. |
323
420
 
@@ -349,7 +446,7 @@ http.listen(3000)
349
446
 
350
447
  Queries return complete, ordered snapshots of a collection or prefix. Defaults bound a query to 10,000 records and 8 MiB of encoded keys and values, and a record to 256 KiB. Exceeding a bound produces `QUERY_LIMIT`. These are safety bounds, not throughput guarantees.
351
448
 
352
- A qualification workload on Node 24.21.0 and macOS arm64 used 1,000 numeric records, three subscribers, and ten writes. Identical queries shared one producer.
449
+ A qualification workload on Node 24.21.0 and macOS arm64 used 1,000 numeric records, three subscribers, and ten writes. Identical queries shared one evaluation.
353
450
 
354
451
  | Measurement | Observed sample |
355
452
  | --- | --- |
@@ -359,6 +456,6 @@ A qualification workload on Node 24.21.0 and macOS arm64 used 1,000 numeric reco
359
456
  | RSS when ready / after the workload | 336 MB / 1.10 GB |
360
457
  | Encoded snapshot | 28.9 KB |
361
458
 
362
- Each changed revision still requires a complete scan. RSS includes the compiled runtime and is not a retained-heap measurement. These observations describe one workload; measure representative scan and delivery time, memory, and storage growth before raising limits. Acceptance receipts and World history accumulate for the dataset lifetime.
459
+ In this workload, each changed revision makes a collection-prefix subscription scan its whole collection. A function query first compares storage roots and the fields it read, and runs again only when they changed. RSS includes the compiled runtime and is not a retained-heap measurement. These observations describe one workload; measure representative scan and delivery time, memory, and storage growth before raising limits. Acceptance receipts and World history accumulate for the dataset lifetime.
363
460
 
364
461
  A qualified release artifact's `qualification.json` records its own sample, source revision, package sizes, and checksum. See the [README](README.md) for supported runtimes and current release scope.
package/CHANGELOG.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  The first typed sync release candidate provides shared Standard Schema contracts, live ordered collections, named atomic server mutations, scoped authentication and authorization, durable SQLite storage, retained acceptance receipts, reconnect recovery, and optional React bindings.
6
6
 
7
- The package ships ESM client, server, and React exports with public TypeScript declarations and a compiled engine worker. Node 24.15–24.x is required for the server. Complete vanilla TypeScript and React task boards use the same contract and storage.
7
+ The package ships ESM client, server, and React exports with public TypeScript declarations and a compiled engine worker. Node 24.15 to 24.x is required for the server. Complete vanilla TypeScript and React task boards use the same contract and storage.
8
8
 
9
9
  Subscriptions share a bounded producer for identical scope, collection, and prefix queries. Each subscriber retains its own authorization checks and cancellation lifetime. Query limits fail the affected subscription while independent queries continue.
10
10
 
package/README.md CHANGED
@@ -187,7 +187,7 @@ export function App() {
187
187
  }
188
188
  ```
189
189
 
190
- `useCollection` cleans up with the component. `useDatabase` gives components the same typed database for writes. The application owns and closes the connection. See the [React task board](examples/task-board/react.tsx) for forms, connection feedback, and write recovery.
190
+ `useCollection` cleans up with the component. `useDatabase` gives components the same typed database for writes. Your application opens and closes the connection. See the [React task board](examples/task-board/react.tsx) for forms, connection feedback, and write recovery.
191
191
 
192
192
  ### Retry without repeating a change
193
193
 
@@ -213,7 +213,7 @@ An accepted duplicate returns the original result, including after a server rest
213
213
 
214
214
  Adding live data often means writing the same contract in several places: database models, API handlers, client types, subscription messages, and UI state. Every new feature has to keep those pieces in agreement.
215
215
 
216
- Spacewave gives that work a shared starting point. Define collections and mutations once with [Standard Schema](https://standardschema.dev/) validators, such as Zod. The server validates writes, TypeScript infers the client API, and subscriptions deliver accepted state to each view. Your application chooses its authentication provider, access policy, and interface.
216
+ With Spacewave, you write that contract once. Define collections and mutations once with [Standard Schema](https://standardschema.dev/) validators, such as Zod. The server validates writes, TypeScript infers the client API, and subscriptions deliver accepted state to each view. Your application chooses its authentication provider, access policy, and interface.
217
217
 
218
218
  ## Architecture
219
219
 
@@ -224,9 +224,9 @@ flowchart LR
224
224
  Server <-->|Transactions| Storage[SQLite-backed World]
225
225
  ```
226
226
 
227
- The Node server authenticates connections, selects each caller's scope, and checks collection permissions on operations and subscription deliveries. Accepted writes commit to a Spacewave World, the engine's durable dataset stored in SQLite. Each dataset directory permits one server writer.
227
+ The Node server authenticates connections, selects each caller's scope, and checks collection permissions on operations and subscription deliveries. Accepted writes commit to a Spacewave World, the engine's durable dataset stored in SQLite. Only one server at a time can write to a dataset directory.
228
228
 
229
- Clients receive whole-collection or prefix snapshots with `loading`, `current`, `stale`, or `error` status. Reconnection refreshes subscriptions; durable write receipts support recovery when acceptance is uncertain.
229
+ Clients receive whole-collection or prefix snapshots with `loading`, `current`, `stale`, or `error` status. Reconnecting refreshes each subscription. The server keeps a receipt for every accepted write, so a client that is unsure whether a write went through can retry it safely.
230
230
 
231
231
  The npm package includes its compiled engine, which runs in a dedicated Node worker. Installing it requires no Go toolchain or lifecycle scripts. See the [API reference](API.md) for the storage, access, and connection contracts.
232
232