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
@@ -0,0 +1,154 @@
1
+ /* Page layout and typography. */
2
+ :root {
3
+ color-scheme: light;
4
+ font-family: system-ui, sans-serif;
5
+ color: #18282d;
6
+ background: #f3f6f4;
7
+ }
8
+
9
+ * {
10
+ box-sizing: border-box;
11
+ }
12
+
13
+ body {
14
+ margin: 0;
15
+ }
16
+
17
+ main {
18
+ width: min(720px, 100%);
19
+ padding: 36px 24px;
20
+ margin: 4vh auto;
21
+ }
22
+
23
+ nav {
24
+ display: flex;
25
+ gap: 18px;
26
+ margin-bottom: 32px;
27
+ }
28
+
29
+ a {
30
+ color: #285f4d;
31
+ text-underline-offset: 4px;
32
+ }
33
+
34
+ h1 {
35
+ font-size: clamp(2rem, 6vw, 3rem);
36
+ line-height: 1.1;
37
+ margin-bottom: 12px;
38
+ letter-spacing: -0.04em;
39
+ }
40
+
41
+ p {
42
+ line-height: 1.6;
43
+ color: #4c6065;
44
+ }
45
+
46
+ .status {
47
+ display: inline-block;
48
+ font-size: 0.85rem;
49
+ background: #dcebe2;
50
+ padding: 5px 12px;
51
+ border-radius: 24px;
52
+ }
53
+
54
+ /* Input controls and their interaction states. */
55
+ form {
56
+ display: flex;
57
+ gap: 10px;
58
+ align-items: end;
59
+ margin: 30px 0;
60
+ }
61
+
62
+ label {
63
+ flex: 1;
64
+ font-size: 0.9rem;
65
+ font-weight: 600;
66
+ }
67
+
68
+ input {
69
+ display: block;
70
+ width: 100%;
71
+ font: inherit;
72
+ margin-top: 8px;
73
+ padding: 12px;
74
+ border: 1px solid #a2b7ad;
75
+ border-radius: 8px;
76
+ background: white;
77
+ }
78
+
79
+ button,
80
+ .button {
81
+ font: inherit;
82
+ color: white;
83
+ background: #285f4d;
84
+ padding: 12px 16px;
85
+ border: 0;
86
+ border-radius: 8px;
87
+ cursor: pointer;
88
+ }
89
+
90
+ button:disabled {
91
+ opacity: 0.55;
92
+ cursor: default;
93
+ }
94
+
95
+ :focus-visible {
96
+ outline: 3px solid #2b7dbe;
97
+ outline-offset: 3px;
98
+ }
99
+
100
+ /* Accepted task rows and write feedback. */
101
+ .tasks {
102
+ list-style: none;
103
+ padding: 0;
104
+ border-top: 1px solid #cddbd3;
105
+ }
106
+
107
+ .task {
108
+ display: flex;
109
+ gap: 16px;
110
+ justify-content: space-between;
111
+ align-items: center;
112
+ padding: 18px 0;
113
+ border-bottom: 1px solid #cddbd3;
114
+ }
115
+
116
+ .task span {
117
+ overflow-wrap: anywhere;
118
+ }
119
+
120
+ .task[data-done='true'] span {
121
+ text-decoration: line-through;
122
+ color: #63756b;
123
+ }
124
+
125
+ .notice {
126
+ padding: 16px;
127
+ background: #fff0d2;
128
+ border-radius: 8px;
129
+ overflow-wrap: anywhere;
130
+ }
131
+
132
+ .notice button {
133
+ margin-top: 10px;
134
+ }
135
+
136
+ /* Keep the form usable on narrow screens. */
137
+ @media (max-width: 460px) {
138
+ main {
139
+ padding: 24px 16px;
140
+ margin-top: 0;
141
+ }
142
+
143
+ form {
144
+ flex-wrap: wrap;
145
+ }
146
+
147
+ form label {
148
+ flex-basis: 100%;
149
+ }
150
+
151
+ form button {
152
+ width: 100%;
153
+ }
154
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2024",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "allowImportingTsExtensions": true,
10
+ "skipLibCheck": true,
11
+ "types": ["node", "react"],
12
+ "lib": ["ES2024", "DOM", "ESNext.Disposable"]
13
+ },
14
+ "include": ["*.ts", "*.tsx"]
15
+ }
@@ -0,0 +1,17 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Task board · Spacewave</title>
7
+ <link rel="stylesheet" href="/style.css" />
8
+ </head>
9
+
10
+ <body>
11
+ <main id="app">
12
+ <p role="status">Connecting…</p>
13
+ </main>
14
+
15
+ <script type="module" src="/vanilla.js"></script>
16
+ </body>
17
+ </html>
@@ -0,0 +1,190 @@
1
+ import { connect, SyncError } from 'spacewave'
2
+
3
+ import { schema } from './schema.ts'
4
+
5
+ // Keep the fixed page scaffold separate from task values rendered with textContent.
6
+ const app = document.querySelector<HTMLElement>('#app')!
7
+ app.innerHTML = `
8
+ <nav>
9
+ <a href="/">Vanilla</a>
10
+ <a href="/react">React</a>
11
+ </nav>
12
+
13
+ <h1>Task board</h1>
14
+ <p>Open two tabs. Add or complete a task and watch both boards update.</p>
15
+ <div class="status" role="status">Connecting…</div>
16
+
17
+ <form>
18
+ <label>
19
+ Add a task
20
+ <input name="title" required maxlength="120" autocomplete="off">
21
+ </label>
22
+ <button class="button">Add task</button>
23
+ </form>
24
+
25
+ <div class="notice" role="alert" hidden></div>
26
+ <p id="freshness" role="status"></p>
27
+ <ul class="tasks"></ul>
28
+ `
29
+
30
+ // Bind the controls used by connection, write, and subscription callbacks.
31
+ const form = app.querySelector<HTMLFormElement>('form')!
32
+ const input = app.querySelector<HTMLInputElement>('input')!
33
+ const status = app.querySelector<HTMLElement>('.status')!
34
+ const notice = app.querySelector<HTMLElement>('.notice')!
35
+ const freshness = app.querySelector<HTMLElement>('#freshness')!
36
+ const list = app.querySelector<HTMLElement>('.tasks')!
37
+
38
+ // Pending form work stays separate from the accepted collection snapshot.
39
+ let pending = false
40
+ let ready = false
41
+
42
+ /** updateButtons permits edits only while connected and without a pending write. */
43
+ function updateButtons(): void {
44
+ for (const button of app.querySelectorAll<HTMLButtonElement>('button')) {
45
+ button.disabled = pending || !ready || button.dataset.done === 'true'
46
+ }
47
+ }
48
+
49
+ /** runWrite retains the original write closure so Retry preserves its input and ID. */
50
+ async function runWrite(write: () => Promise<unknown>): Promise<void> {
51
+ // Suspend edits while the server decides this write's result.
52
+ pending = true
53
+ notice.hidden = true
54
+ updateButtons()
55
+
56
+ // Show failures without changing the accepted task rows.
57
+ try {
58
+ await write()
59
+ } catch (error) {
60
+ notice.hidden = false
61
+ notice.replaceChildren(
62
+ document.createTextNode(
63
+ error instanceof Error
64
+ ? error.message
65
+ : 'The change could not complete',
66
+ ),
67
+ )
68
+
69
+ // Retrying an uncertain write reuses its original input and request ID.
70
+ if (error instanceof SyncError && error.code === 'UNCERTAIN') {
71
+ notice.append(document.createTextNode(` Request: ${error.requestId}. `))
72
+ const retry = document.createElement('button')
73
+ retry.textContent = 'Retry'
74
+ retry.onclick = () => {
75
+ void runWrite(write)
76
+ }
77
+ notice.append(retry)
78
+ }
79
+ } finally {
80
+ pending = false
81
+ updateButtons()
82
+ }
83
+ }
84
+
85
+ /** start connects the board and releases its subscriptions when the page leaves. */
86
+ async function start(): Promise<void> {
87
+ // Open one connection for the page and use its typed task collection.
88
+ const url = new URL('/sync', location.href)
89
+ url.protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
90
+ const db = await connect({
91
+ url: url.href,
92
+ schema,
93
+ getAccessToken: () => 'local-demo',
94
+ })
95
+ const todos = db.collection('todos')
96
+
97
+ // Keep connection feedback and edit availability in sync.
98
+ const stopConnection = db.connection.subscribe((connection) => {
99
+ ready = connection.status === 'ready'
100
+ status.textContent =
101
+ connection.status === 'ready'
102
+ ? 'Connected'
103
+ : connection.status === 'reconnecting'
104
+ ? 'Reconnecting…'
105
+ : 'Connection closed'
106
+ updateButtons()
107
+ })
108
+
109
+ // Replace the rendered list with each complete server snapshot.
110
+ const stop = todos.subscribe(
111
+ {},
112
+ {
113
+ next: (state) => {
114
+ // Describe freshness separately from the retained task values.
115
+ freshness.textContent =
116
+ state.status === 'loading'
117
+ ? 'Loading tasks…'
118
+ : state.status === 'stale'
119
+ ? 'Showing the last saved view while reconnecting…'
120
+ : state.status === 'error'
121
+ ? (state.error?.message ?? 'Tasks could not load')
122
+ : state.data.length
123
+ ? 'All changes saved'
124
+ : 'No tasks yet'
125
+
126
+ // Render task titles as text and wire each row's completion action.
127
+ list.replaceChildren(
128
+ ...state.data.map(({ key, value }) => {
129
+ // Build the row from the accepted task value.
130
+ const row = document.createElement('li')
131
+ row.className = 'task'
132
+ row.dataset.done = String(value.done)
133
+ const title = document.createElement('span')
134
+ title.textContent = value.title
135
+
136
+ // Keep a completion request's ID stable if the user retries it.
137
+ const complete = document.createElement('button')
138
+ complete.textContent = value.done ? 'Completed' : 'Complete'
139
+ complete.dataset.done = String(value.done)
140
+ complete.onclick = () => {
141
+ const requestId = crypto.randomUUID()
142
+ void runWrite(() =>
143
+ db.mutate('completeTodo', { id: key }, { requestId }),
144
+ )
145
+ }
146
+ row.append(title, complete)
147
+ return row
148
+ }),
149
+ )
150
+ updateButtons()
151
+ },
152
+ },
153
+ )
154
+
155
+ // Capture a new task's input once so recovery can repeat the exact write.
156
+ form.onsubmit = (event) => {
157
+ // Accept a nonempty title only when edits are available.
158
+ event.preventDefault()
159
+ const title = input.value.trim()
160
+ if (!title || pending || !ready) return
161
+
162
+ // Clear the input after acceptance, preserving the IDs inside the closure.
163
+ const key = crypto.randomUUID()
164
+ const requestId = crypto.randomUUID()
165
+ void runWrite(async () => {
166
+ await todos.put(key, { title, done: false }, { requestId })
167
+ input.value = ''
168
+ })
169
+ }
170
+
171
+ // Release both subscriptions before closing the page's database.
172
+ window.addEventListener(
173
+ 'pagehide',
174
+ () => {
175
+ stop()
176
+ stopConnection()
177
+ void db.close()
178
+ },
179
+ { once: true },
180
+ )
181
+ }
182
+
183
+ // Disable edits until admission succeeds and make startup failures visible.
184
+ updateButtons()
185
+ void start().catch((error: unknown) => {
186
+ status.textContent = 'Connection failed'
187
+ notice.hidden = false
188
+ notice.textContent =
189
+ error instanceof Error ? error.message : 'The board could not connect'
190
+ })
package/package.json CHANGED
@@ -1,22 +1,60 @@
1
1
  {
2
2
  "name": "spacewave",
3
- "description": "Cross-platform communications daemon and library.",
4
- "version": "0.0.0",
5
- "license": "Apache-2.0",
6
- "author": {
7
- "name": "Aperture Robotics LLC.",
8
- "email": "support@aperture.us",
9
- "url": "http://aperture.us"
3
+ "version": "0.57.6",
4
+ "description": "Typed live collections backed by a durable Spacewave World",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/s4wave/spacewave.git",
8
+ "directory": "packages/spacewave"
9
+ },
10
+ "homepage": "https://github.com/s4wave/spacewave/tree/master/packages/spacewave#readme",
11
+ "bugs": "https://github.com/s4wave/spacewave/issues",
12
+ "type": "module",
13
+ "engines": {
14
+ "node": ">=24.15.0 <25"
10
15
  },
11
- "contributors": [
12
- {
13
- "name": "Christian Stewart",
14
- "email": "christian@aperture.us",
15
- "url": "http://github.com/paralin"
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/types/packages/spacewave/index.d.ts",
19
+ "import": "./dist/index.mjs"
20
+ },
21
+ "./server": {
22
+ "types": "./dist/types/packages/spacewave/server.d.ts",
23
+ "import": "./dist/server.mjs"
24
+ },
25
+ "./react": {
26
+ "types": "./dist/types/packages/spacewave/react.d.ts",
27
+ "import": "./dist/react.mjs"
16
28
  }
17
- ],
18
- "repository": {
19
- "url": "git+ssh://git@github.com/aperturerobotics/spacewave/net.git"
20
29
  },
21
- "type": "module"
30
+ "types": "./dist/types/packages/spacewave/index.d.ts",
31
+ "dependencies": {
32
+ "@standard-schema/spec": "^1.1.0"
33
+ },
34
+ "peerDependencies": {
35
+ "react": ">=19.2.0 <20",
36
+ "@types/react": ">=19.2.0 <20",
37
+ "@types/node": ">=24 <25"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "react": {
41
+ "optional": true
42
+ },
43
+ "@types/react": {
44
+ "optional": true
45
+ },
46
+ "@types/node": {
47
+ "optional": true
48
+ }
49
+ },
50
+ "sideEffects": false,
51
+ "files": [
52
+ "dist",
53
+ "examples",
54
+ "API.md",
55
+ "AGENTS.md",
56
+ "CHANGELOG.md",
57
+ "LICENSE"
58
+ ],
59
+ "license": "Apache-2.0"
22
60
  }