spacewave 0.57.6 → 0.57.8
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 +112 -15
- package/CHANGELOG.md +1 -1
- package/README.md +4 -4
- package/dist/THIRD_PARTY_NOTICES.txt +95 -166
- package/dist/build.json +2 -2
- package/dist/chunks/json-BuGtLL3Z.mjs +2 -0
- package/dist/chunks/node.pb-L2jxLJ0J.mjs +2 -0
- package/dist/engine-worker.mjs +100 -100
- package/dist/index.mjs +1 -1
- package/dist/react.mjs +1 -1
- package/dist/server.mjs +4 -5
- package/dist/types/core/sync/config.d.ts +4 -1
- package/dist/types/packages/spacewave/index.d.ts +4 -0
- package/dist/types/packages/spacewave/react.d.ts +3 -1
- package/dist/types/sdk/sync/app.d.ts +37 -0
- package/dist/types/sdk/sync/query.d.ts +71 -0
- package/dist/types/web/sync/app-hooks.d.ts +31 -0
- package/examples/task-board/package.json +1 -1
- package/examples/task-board/react.tsx +27 -27
- package/package.json +1 -1
- package/dist/chunks/errors-BHnASqFk.mjs +0 -2
- package/dist/chunks/node.pb-ChF26ESv.mjs +0 -2
- package/dist/types/web/sync/index.d.ts +0 -4
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
310
|
-
|
|
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.
|
|
320
|
-
| `as(principal)` | `DatabaseAccess<S>` for
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|