spacewave 0.0.0 → 0.57.6

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.
Files changed (36) hide show
  1. package/AGENTS.md +82 -0
  2. package/API.md +364 -0
  3. package/CHANGELOG.md +11 -0
  4. package/LICENSE +191 -0
  5. package/README.md +349 -0
  6. package/dist/THIRD_PARTY_NOTICES.txt +4497 -0
  7. package/dist/build.json +6 -0
  8. package/dist/chunks/errors-BHnASqFk.mjs +2 -0
  9. package/dist/chunks/node.pb-ChF26ESv.mjs +2 -0
  10. package/dist/chunks/rolldown-runtime-Csi-cuKc.mjs +1 -0
  11. package/dist/engine-worker.mjs +112 -0
  12. package/dist/index.mjs +1 -0
  13. package/dist/react.mjs +1 -0
  14. package/dist/server.mjs +10 -0
  15. package/dist/types/core/sync/config.d.ts +55 -0
  16. package/dist/types/packages/spacewave/index.d.ts +7 -0
  17. package/dist/types/packages/spacewave/react.d.ts +1 -0
  18. package/dist/types/packages/spacewave/server.d.ts +4 -0
  19. package/dist/types/sdk/sync/access.d.ts +15 -0
  20. package/dist/types/sdk/sync/client.d.ts +37 -0
  21. package/dist/types/sdk/sync/errors.d.ts +12 -0
  22. package/dist/types/sdk/sync/json.d.ts +30 -0
  23. package/dist/types/sdk/sync/operation.d.ts +23 -0
  24. package/dist/types/sdk/sync/schema.d.ts +51 -0
  25. package/dist/types/web/sync/index.d.ts +4 -0
  26. package/dist/types/web/sync/react.d.ts +11 -0
  27. package/examples/task-board/package.json +25 -0
  28. package/examples/task-board/react.html +17 -0
  29. package/examples/task-board/react.tsx +235 -0
  30. package/examples/task-board/schema.ts +21 -0
  31. package/examples/task-board/server.ts +133 -0
  32. package/examples/task-board/style.css +154 -0
  33. package/examples/task-board/tsconfig.json +15 -0
  34. package/examples/task-board/vanilla.html +17 -0
  35. package/examples/task-board/vanilla.ts +190 -0
  36. package/package.json +54 -16
package/README.md ADDED
@@ -0,0 +1,349 @@
1
+ <div align="center">
2
+
3
+ # Spacewave
4
+
5
+ **Live data for TypeScript applications.**
6
+
7
+ [![npm](https://img.shields.io/npm/v/spacewave)](https://www.npmjs.com/package/spacewave)
8
+ [![Node.js 24](https://img.shields.io/badge/node-%3E%3D24.15%20%3C25-5FA04E)](#getting-started)
9
+ [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)
10
+
11
+ [Get started](#getting-started) · [Showcase](#showcase) · [API reference](API.md) · [Task board](examples/task-board) · [Contributing](#contributing)
12
+
13
+ </div>
14
+
15
+ Spacewave keeps application data in sync between a Node.js server and TypeScript clients. Define your data with a shared schema, read and write typed collections, and subscribe to changes from the browser or React.
16
+
17
+ Build shared task boards, chat feeds, job dashboards, and settings that follow users between tabs. Accepted writes persist in SQLite, mutations run in transactions, and your authentication and authorization functions control access. You run the server and keep the data in your own application.
18
+
19
+ ```sh
20
+ npm install spacewave
21
+ ```
22
+
23
+ ## Showcase
24
+
25
+ These examples share a schema and a connected client named `db`. Server examples use `server`, created with that same schema. The [setup below](#getting-started) defines their collections and connects them.
26
+
27
+ ### Shared task lists
28
+
29
+ Write a task in one tab and watch it appear in another. Collection names and record values are checked by TypeScript.
30
+
31
+ ```ts
32
+ const todos = db.collection('todos')
33
+
34
+ const unsubscribe = todos.subscribe(
35
+ {},
36
+ {
37
+ next: ({ status, data }) => console.log(status, data),
38
+ },
39
+ )
40
+
41
+ await todos.put('welcome', { title: 'Try Spacewave', done: false })
42
+ await todos.put('welcome', { title: 'Try Spacewave', done: true })
43
+ ```
44
+
45
+ Subscriptions deliver complete snapshots ordered by key. Call `unsubscribe()` when the view leaves; the client handles reconnection.
46
+
47
+ ### Chat feeds
48
+
49
+ Use a key prefix to subscribe to one room's messages. Timestamp-prefixed keys keep the feed in key order; a UUID distinguishes messages sent at the same time.
50
+
51
+ ```ts
52
+ const messages = db.collection('messages')
53
+ const room = 'general/'
54
+
55
+ const unsubscribe = messages.subscribe(
56
+ { prefix: room },
57
+ {
58
+ next: ({ data }) => console.table(data.map(({ value }) => value)),
59
+ },
60
+ )
61
+
62
+ await messages.put(
63
+ `${room}${new Date().toISOString()}/${crypto.randomUUID()}`,
64
+ {
65
+ text: 'Anyone up for a quick demo?',
66
+ },
67
+ )
68
+ ```
69
+
70
+ Messages persist across server restarts. A prefix selects the feed; collection permissions and the authenticated scope control access. This example orders messages by client timestamp, not server arrival time.
71
+
72
+ ### Background job progress
73
+
74
+ A worker can write through the same collection API that browsers use. Here a server-side job reports its progress after processing a batch:
75
+
76
+ ```ts
77
+ const jobs = server
78
+ .as({ subject: 'import-worker', scope: 'demo' })
79
+ .collection('jobs')
80
+
81
+ await jobs.put('import/contacts', { completed: 50, total: 100 })
82
+ ```
83
+
84
+ Subscribe from the dashboard to receive progress as it changes:
85
+
86
+ ```ts
87
+ const unsubscribe = db.collection('jobs').subscribe(
88
+ { prefix: 'import/' },
89
+ {
90
+ next: ({ data }) => console.table(data),
91
+ },
92
+ )
93
+ ```
94
+
95
+ `server.as(principal)` applies the same authorization policy as client operations. Grant the worker access to the collections its job needs.
96
+
97
+ ### Shared settings
98
+
99
+ Keep a team's preferences in one record. Connected settings views can subscribe to changes; a one-time read uses `get()`.
100
+
101
+ ```ts
102
+ const settings = db.collection('settings')
103
+
104
+ await settings.put('appearance', { theme: 'dark', compact: true })
105
+ const appearance = await settings.get('appearance')
106
+ console.log(appearance?.theme) // 'dark'
107
+ ```
108
+
109
+ ### Separate team datasets
110
+
111
+ Each authenticated scope has its own collection data. The same record key can hold different values for different teams:
112
+
113
+ ```ts
114
+ const teamA = server.as({ subject: 'service', scope: 'team-a' })
115
+ const teamB = server.as({ subject: 'service', scope: 'team-b' })
116
+
117
+ await teamA
118
+ .collection('todos')
119
+ .put('welcome', { title: 'Plan the launch', done: false })
120
+ await teamB
121
+ .collection('todos')
122
+ .put('welcome', { title: 'Review the design', done: false })
123
+ ```
124
+
125
+ Your server's verifier assigns the caller's scope, and its policy grants access. The setup below permits the example scopes; use your application's token verification and team membership rules before serving users.
126
+
127
+ ### Atomic counters
128
+
129
+ When a change depends on existing data, use a named mutation. Its reads and writes run in one server transaction. Add this handler to the server's `mutations` option:
130
+
131
+ ```ts
132
+ import type { MutationHandlers, Principal } from 'spacewave'
133
+
134
+ const mutations: MutationHandlers<typeof schema, Principal> = {
135
+ increment: async (context, { key }) => {
136
+ const counters = context.collection('counters')
137
+ const value = ((await counters.get(key)) ?? 0) + 1
138
+ await counters.put(key, value)
139
+ return value
140
+ },
141
+ }
142
+ ```
143
+
144
+ Then call it from a client:
145
+
146
+ ```ts
147
+ const views = await db.mutate('increment', { key: 'page-views' })
148
+ ```
149
+
150
+ The schema declares the input and output types. A mutation can also update several records or collections in the same transaction.
151
+
152
+ ### Live React views
153
+
154
+ The optional React entry binds hooks to your schema. Pass an existing connection to the provider and read live collections inside it:
155
+
156
+ ```tsx
157
+ import { createSyncContext } from 'spacewave/react'
158
+
159
+ const { SyncProvider, useCollection } = createSyncContext(schema)
160
+
161
+ function Tasks() {
162
+ const todos = useCollection('todos')
163
+ if (todos.status === 'loading') return <p>Loading tasks…</p>
164
+ if (todos.status === 'error')
165
+ return <p role="alert">{todos.error?.message}</p>
166
+
167
+ return (
168
+ <>
169
+ {todos.status === 'stale' && (
170
+ <p>Reconnecting. Showing the last saved view.</p>
171
+ )}
172
+ <ul>
173
+ {todos.data.map(({ key, value }) => (
174
+ <li key={key}>{value.title}</li>
175
+ ))}
176
+ </ul>
177
+ </>
178
+ )
179
+ }
180
+
181
+ export function App() {
182
+ return (
183
+ <SyncProvider db={db}>
184
+ <Tasks />
185
+ </SyncProvider>
186
+ )
187
+ }
188
+ ```
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.
191
+
192
+ ### Retry without repeating a change
193
+
194
+ A connection can fail after the server accepts a write but before the client receives its result. If the call reports `UNCERTAIN`, retry the exact operation with its original request ID and input:
195
+
196
+ ```ts
197
+ import { SyncError } from 'spacewave'
198
+
199
+ const requestId = crypto.randomUUID()
200
+ const input = { key: 'page-views' }
201
+
202
+ try {
203
+ await db.mutate('increment', input, { requestId })
204
+ } catch (error) {
205
+ if (!(error instanceof SyncError) || error.code !== 'UNCERTAIN') throw error
206
+ await db.mutate('increment', input, { requestId })
207
+ }
208
+ ```
209
+
210
+ An accepted duplicate returns the original result, including after a server restart. Reusing the ID with different input fails with `CONFLICT`. The [retry reference](API.md#uncertain-writes) explains how to retain unresolved writes for later recovery.
211
+
212
+ ## Motivation
213
+
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
+
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.
217
+
218
+ ## Architecture
219
+
220
+ ```mermaid
221
+ flowchart LR
222
+ Browser[Browser or React] <-->|WebSocket| Server[Node.js server]
223
+ Jobs[Server-side code] -->|Collection API| Server
224
+ Server <-->|Transactions| Storage[SQLite-backed World]
225
+ ```
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.
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.
230
+
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
+
233
+ ## Getting started
234
+
235
+ Use **Node.js `>=24.15.0 <25`** for the server and **ESM** for your application. Browser clients use the native WebSocket API. The optional React bindings require React 19.2 or later within 19.x and its matching type packages.
236
+
237
+ For a complete application, install Spacewave and copy the bundled task board:
238
+
239
+ ```sh
240
+ npm install spacewave
241
+ cp -R node_modules/spacewave/examples/task-board ./task-board
242
+ cd task-board
243
+ npm install
244
+ npm start
245
+ ```
246
+
247
+ Open [the vanilla board](http://127.0.0.1:8787) and [the React board](http://127.0.0.1:8787/react). Add a task in one and complete it in the other. Restart the server to see the saved tasks from its `.data` directory.
248
+
249
+ For your own application, the following setup supports the showcase examples. Install Zod with `npm install zod`. Server TypeScript projects also need `@types/node` 24.x and `"node"` in `compilerOptions.types`.
250
+
251
+ <details>
252
+ <summary><strong>Shared schema, server, and client setup</strong></summary>
253
+
254
+ **`schema.ts`** defines the contract imported by both server and client:
255
+
256
+ ```ts
257
+ import { defineSchema } from 'spacewave'
258
+ import { z } from 'zod'
259
+
260
+ export const schema = defineSchema({
261
+ id: 'showcase',
262
+ version: 1,
263
+ collections: {
264
+ todos: z.object({ title: z.string(), done: z.boolean() }),
265
+ messages: z.object({ text: z.string() }),
266
+ jobs: z.object({ completed: z.number(), total: z.number() }),
267
+ settings: z.object({
268
+ theme: z.enum(['light', 'dark']),
269
+ compact: z.boolean(),
270
+ }),
271
+ counters: z.number().int(),
272
+ },
273
+ mutations: {
274
+ increment: {
275
+ input: z.object({ key: z.string() }),
276
+ output: z.number().int(),
277
+ },
278
+ },
279
+ })
280
+ ```
281
+
282
+ **`server.ts`** opens storage and a WebSocket endpoint. Place the `mutations` declaration from the atomic counter example in this module before `createServer`:
283
+
284
+ ```ts
285
+ import { SyncError } from 'spacewave'
286
+ import { createServer } from 'spacewave/server'
287
+
288
+ import { schema } from './schema.js'
289
+
290
+ const server = await createServer({
291
+ directory: './data',
292
+ schema,
293
+ authenticate: async (token) => {
294
+ if (token !== 'local-demo')
295
+ throw new SyncError('AUTHENTICATION', 'Sign in again')
296
+ return { subject: 'demo-user', scope: 'demo' }
297
+ },
298
+ authorize: ({ scope }) => ['demo', 'team-a', 'team-b'].includes(scope),
299
+ mutations,
300
+ })
301
+
302
+ await server.listen({
303
+ port: 8787,
304
+ allowedOrigins: ['http://localhost:5173', 'http://127.0.0.1:5173'],
305
+ })
306
+ ```
307
+
308
+ Replace the local demo verifier and policy before serving other users. Set `allowedOrigins` to your frontend's origin and use `wss://` with HTTPS. You can also attach sync to an [existing HTTP server](API.md#hosting-and-lifetime).
309
+
310
+ **`db.ts`** opens the client connection:
311
+
312
+ ```ts
313
+ import { connect } from 'spacewave'
314
+
315
+ import { schema } from './schema.js'
316
+
317
+ export const db = await connect({
318
+ url: 'ws://127.0.0.1:8787/sync',
319
+ schema,
320
+ getAccessToken: () => 'local-demo',
321
+ })
322
+ ```
323
+
324
+ Import `db` and `schema` into the client examples that use them. Compile with your application's TypeScript setup and run the server output with Node.js. Call `await db.close()` and `await server.close()` at their respective application shutdowns. Importing package entries is safe during server rendering; keep calls to `connect()` in client startup code.
325
+
326
+ </details>
327
+
328
+ Prereleases are available with `npm install spacewave@next`. To build a tarball yourself, follow the [repository setup](../../README.md), then run `bun run build:sync` and `bun run pack:sync` from the repository root.
329
+
330
+ ## Current scope
331
+
332
+ The sync API is in early development and is intended for evaluating small live datasets:
333
+
334
+ - Queries return complete collection or prefix snapshots. Defaults cap them at 10,000 records and 8 MiB per snapshot, with 256 KiB per record.
335
+ - Clients retain their last snapshot in memory while reconnecting. Writes require the server; durable offline replicas, offline queues, and optimistic updates are outside this release.
336
+ - Access policies grant reads and writes per collection within a scope. Row-level authorization and SQL queries are outside this release.
337
+ - Schema changes require an explicit migration. Write receipts remain for the dataset lifetime.
338
+
339
+ Read the [measured workload results](API.md#limits-and-performance) before growing a dataset, and the [release notes](CHANGELOG.md) for current capabilities.
340
+
341
+ ## Contributing
342
+
343
+ Bug reports, small reproductions, and feedback on the API are welcome in [GitHub issues](https://github.com/s4wave/spacewave/issues). Include your package and Node versions, the error code, and enough code to reproduce the behavior.
344
+
345
+ For code changes, follow the [repository setup](../../README.md). Run `bun run build:sync` to build the package. `bun run qualify:sync` checks an installed tarball, persistence, reconnection, and the Chromium and WebKit examples. The [agent guide](AGENTS.md) covers integration rules for coding assistants.
346
+
347
+ ## License
348
+
349
+ [Apache-2.0](LICENSE). Distributed packages include third-party notices in `dist/THIRD_PARTY_NOTICES.txt`.