@standardagents/code-web 0.14.1-issue.663.fd4c3f → 0.14.4-branch.9e48e914.ee06ef

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 (37) hide show
  1. package/README.md +9 -1
  2. package/dist/build.json +1 -1
  3. package/dist/licenses/jco-transpile.txt +220 -0
  4. package/dist/manifest.json +1 -1
  5. package/dist/manifest.sig +1 -1
  6. package/dist/pkg/standard_web.d.ts +44 -6
  7. package/dist/pkg/standard_web.js +292 -20
  8. package/dist/pkg/standard_web_bg.wasm +0 -0
  9. package/dist/pkg/standard_web_bg.wasm.d.ts +11 -6
  10. package/dist/plugins/clock-cells/clock_cells.core.wasm +0 -0
  11. package/dist/plugins/clock-cells/clock_cells.wasm +0 -0
  12. package/dist/plugins/clock-cells/standard-plugin.json +11 -0
  13. package/dist/plugins/host/transpile.js +51 -0
  14. package/dist/plugins/host/ui-jspi.js +14851 -0
  15. package/dist/plugins/host/ui.core2.wasm +0 -0
  16. package/dist/plugins/host/ui.core3.wasm +0 -0
  17. package/dist/plugins/host/ui.js +14675 -0
  18. package/dist/plugins/host/world.js +172 -0
  19. package/dist/plugins/jco/cli.js +141 -0
  20. package/dist/plugins/jco/common.js +16 -0
  21. package/dist/plugins/jco/config.js +7 -0
  22. package/dist/plugins/jco/environment.js +24 -0
  23. package/dist/plugins/jco/filesystem.js +339 -0
  24. package/dist/plugins/jco/in-memory-filesystem.js +620 -0
  25. package/dist/plugins/jco/io.js +366 -0
  26. package/dist/plugins/jco/js-component-bindgen-component.core.wasm +0 -0
  27. package/dist/plugins/jco/js-component-bindgen-component.core2.wasm +0 -0
  28. package/dist/plugins/jco/js-component-bindgen-component.js +14272 -0
  29. package/dist/plugins/jco/opfs-filesystem.js +245 -0
  30. package/dist/plugins/jco/random.js +51 -0
  31. package/dist/plugins/plasma-pixels/plasma_pixels.core.wasm +0 -0
  32. package/dist/plugins/plasma-pixels/plasma_pixels.wasm +0 -0
  33. package/dist/plugins/plasma-pixels/standard-plugin.json +11 -0
  34. package/dist/plugins/runtime.js +1044 -0
  35. package/dist/plugins/samples.json +81 -0
  36. package/dist/plugins/worker.js +411 -0
  37. package/package.json +8 -2
@@ -0,0 +1,81 @@
1
+ {
2
+ "schema": 2,
3
+ "transpiler": "@bytecodealliance/jco-transpile@0.15.0",
4
+ "world": {
5
+ "glue": {
6
+ "file": "ui.js",
7
+ "sha256": "24c4ba57a14d27d8fda334210c4b5225f1b196ed43f8680442954f07080294ba"
8
+ },
9
+ "jspi": {
10
+ "file": "ui-jspi.js",
11
+ "sha256": "24e6e96752a139da58b703f6b2ae7551ae011783ed6ca895cf343e1a355da8e9"
12
+ },
13
+ "shims": [
14
+ {
15
+ "file": "ui.core2.wasm",
16
+ "sha256": "f3b18532f5fa5f5e31522891929d243dd383df225120106933cce6b7b31960c0"
17
+ },
18
+ {
19
+ "file": "ui.core3.wasm",
20
+ "sha256": "7ec25df2026c03fdd7c51abe4522e357d5888c0b2ba4b45cf8bf502275da1c60"
21
+ }
22
+ ]
23
+ },
24
+ "plugins": [
25
+ {
26
+ "id": "clock-cells",
27
+ "dir": "clock-cells",
28
+ "manifest": {
29
+ "apiVersion": 2,
30
+ "kind": "ui",
31
+ "id": "clock-cells",
32
+ "version": "0.1.0",
33
+ "module": "clock_cells.wasm",
34
+ "surfaces": [
35
+ {
36
+ "id": "card",
37
+ "anchor": "sidebar.card",
38
+ "model": "cells",
39
+ "height": 2
40
+ }
41
+ ],
42
+ "grants": {}
43
+ },
44
+ "component": {
45
+ "file": "clock_cells.wasm",
46
+ "sha256": "80d8a92f866f3bd906dfdde535e5b6489613ebffa4b04b0029d71182f2af28b9"
47
+ },
48
+ "core": {
49
+ "file": "clock_cells.core.wasm",
50
+ "sha256": "df640416b7ff87c0bd7dc9982f29baa6fdf8c856e13f28484e13727cfd3823ac"
51
+ }
52
+ },
53
+ {
54
+ "id": "plasma-pixels",
55
+ "dir": "plasma-pixels",
56
+ "manifest": {
57
+ "apiVersion": 2,
58
+ "kind": "ui",
59
+ "id": "plasma-pixels",
60
+ "version": "0.1.0",
61
+ "module": "plasma_pixels.wasm",
62
+ "surfaces": [
63
+ {
64
+ "id": "stage",
65
+ "anchor": "stage",
66
+ "model": "pixels"
67
+ }
68
+ ],
69
+ "grants": {}
70
+ },
71
+ "component": {
72
+ "file": "plasma_pixels.wasm",
73
+ "sha256": "cc80f9e6fdeb7433cf3f1ab7787f7192082a2a86a38d8aa535f7d6c934c2819d"
74
+ },
75
+ "core": {
76
+ "file": "plasma_pixels.core.wasm",
77
+ "sha256": "0cf0a23eceb1acc34db76b6e20f773229c52e0fc453b4324ea302347c899e6d0"
78
+ }
79
+ }
80
+ ]
81
+ }
@@ -0,0 +1,411 @@
1
+ // One plugin's Worker (docs/plugin-runtime.md, "The browser runtime").
2
+ //
3
+ // The viewer starts one module Worker per plugin from its own verified
4
+ // bundle path and sends it plain messages: `{ m: <json>, b: [buffers] }`.
5
+ // The Worker gets the plugin's world core module (a sample's from the
6
+ // bundle, checked against the index; an account package's by transpiling
7
+ // its verified component in the viewer, cached per component hash),
8
+ // instantiates it through the viewer's own reference glue (host/world.js)
9
+ // with the host imports in runtime.js, and calls `activate`, `frame()` and
10
+ // `event()` only when the viewer asks. It never wakes on its own: an idle
11
+ // plugin costs nothing here.
12
+ //
13
+ // The same script, started by a plugin Worker with a `transpile` message,
14
+ // is the transpile Worker: it loads jco's transpiler, extracts the world
15
+ // core module, answers, and is terminated. It runs under the Worker policy
16
+ // the site gives this path, and only the plugin Worker that needs a
17
+ // transpile pays for loading jco.
18
+ //
19
+ // Calls into the plugin run one at a time, in the order the viewer sent
20
+ // them. Under JSPI an account call suspends the plugin until the viewer
21
+ // answers; what arrives meanwhile waits its turn, except the answers
22
+ // themselves, released buffers and view state.
23
+ //
24
+ // The viewer owns the budgets: this Worker reports how long each `frame()`
25
+ // took, less the time it waited for the account, and the viewer terminates
26
+ // a Worker that overruns or hangs.
27
+ import { Mirror, PluginHost, witEvent } from './runtime.js'
28
+ import { instantiatePlugin } from './host/world.js'
29
+ import { extractMain } from './host/transpile.js'
30
+
31
+ const CACHE = 'standard-plugin-core-v1'
32
+
33
+ let epoch = 0
34
+ let host = null
35
+ let plugin = null
36
+ let memory = () => null
37
+ let memoryLimit = 64 * 1024 * 1024
38
+ let dead = false
39
+ /** Work for the plugin, in order; `running` while a call is suspended. */
40
+ const queue = []
41
+ let running = false
42
+ /** A call into the plugin is on the stack or suspended. */
43
+ let calling = false
44
+ /** What arrived before the plugin was instantiated. */
45
+ let early = []
46
+ /** Account calls in flight, by id, and the time spent waiting on them. */
47
+ const pending = new Map()
48
+ let nextCall = 1
49
+ let suspendedSince = null
50
+ let suspendedMs = 0
51
+
52
+ function post(message, buffer) {
53
+ const data = { m: JSON.stringify({ ...message, epoch }) }
54
+ if (buffer) {
55
+ data.b = [buffer]
56
+ self.postMessage(data, [buffer])
57
+ } else {
58
+ self.postMessage(data)
59
+ }
60
+ }
61
+
62
+ function describe(error) {
63
+ return error instanceof Error ? `${error.name}: ${error.message}` : String(error)
64
+ }
65
+
66
+ /** A trap, or a call that grew memory past the limit, ends this instance. */
67
+ function trapped(error) {
68
+ if (dead) return
69
+ dead = true
70
+ queue.length = 0
71
+ post({ t: 'trap', error: describe(error) })
72
+ }
73
+
74
+ function checkMemory() {
75
+ const current = memory()
76
+ if (current && current.buffer.byteLength > memoryLimit) {
77
+ trapped(new Error(`linear memory grew to ${current.buffer.byteLength} bytes, over the ${memoryLimit} limit`))
78
+ return false
79
+ }
80
+ return true
81
+ }
82
+
83
+ async function sha256(bytes) {
84
+ const digest = await crypto.subtle.digest('SHA-256', bytes)
85
+ return [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, '0')).join('')
86
+ }
87
+
88
+ /** A file of this bundle, bound to the SHA-256 the index names. The site's
89
+ * verified route checked the whole bundle; this binds each file to the
90
+ * index the viewer read. A non-secure development origin has no WebCrypto
91
+ * and relies on the bundle check alone. */
92
+ async function bundleFile(url, expected) {
93
+ const response = await fetch(url)
94
+ if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`)
95
+ const bytes = new Uint8Array(await response.arrayBuffer())
96
+ if (globalThis.crypto?.subtle) {
97
+ const actual = await sha256(bytes)
98
+ if (actual !== expected) throw new Error(`${url}: SHA-256 ${actual} does not match the index`)
99
+ }
100
+ return bytes
101
+ }
102
+
103
+ // ── the account, through the viewer ─────────────────────────────────────────
104
+
105
+ function ask(call) {
106
+ const id = nextCall++
107
+ return new Promise((resolve, reject) => {
108
+ pending.set(id, { resolve, reject })
109
+ if (calling && pending.size === 1 && suspendedSince === null) {
110
+ suspendedSince = performance.now()
111
+ post({ t: 'suspend', on: true })
112
+ }
113
+ post({ t: 'account', id, call })
114
+ })
115
+ }
116
+
117
+ function answered(id, reply) {
118
+ const waiter = pending.get(id)
119
+ if (!waiter) return
120
+ pending.delete(id)
121
+ if (pending.size === 0 && suspendedSince !== null) {
122
+ suspendedMs += performance.now() - suspendedSince
123
+ suspendedSince = null
124
+ post({ t: 'suspend', on: false })
125
+ }
126
+ waiter.resolve(reply)
127
+ }
128
+
129
+ // ── the world core module ───────────────────────────────────────────────────
130
+
131
+ /** Transpiles in a Worker of its own, so jco's memory goes with it. */
132
+ function transpileElsewhere(component) {
133
+ return new Promise((resolve, reject) => {
134
+ const worker = new Worker(import.meta.url, { type: 'module' })
135
+ worker.onmessage = (event) => {
136
+ worker.terminate()
137
+ const message = JSON.parse(event.data.m)
138
+ if (message.t === 'transpiled') resolve({ bytes: new Uint8Array(event.data.b[0]), ms: message.ms })
139
+ else reject(new Error(message.error))
140
+ }
141
+ worker.onerror = (event) => {
142
+ worker.terminate()
143
+ reject(new Error(event.message || 'the transpile Worker failed'))
144
+ }
145
+ const copy = component.slice().buffer
146
+ worker.postMessage({ m: JSON.stringify({ t: 'transpile' }), b: [copy] }, [copy])
147
+ })
148
+ }
149
+
150
+ async function transpileHere(component) {
151
+ const started = performance.now()
152
+ const jco = await import(new URL('jco/js-component-bindgen-component.js', import.meta.url).href)
153
+ await jco.$init
154
+ const loadedAt = performance.now()
155
+ const bytes = await extractMain(component, jco.generate)
156
+ return { bytes, ms: { load: loadedAt - started, transpile: performance.now() - loadedAt } }
157
+ }
158
+
159
+ function cacheKey(base, entry, world) {
160
+ return new Request(new URL(`plugins/cache/${entry.component.sha256}-${world.glue.sha256.slice(0, 16)}.wasm`, base))
161
+ }
162
+
163
+ async function cached(key) {
164
+ try {
165
+ if (!globalThis.caches) return null
166
+ const response = await (await caches.open(CACHE)).match(key)
167
+ if (!response) return null
168
+ const bytes = new Uint8Array(await response.arrayBuffer())
169
+ const expected = response.headers.get('x-core-sha256')
170
+ if (globalThis.crypto?.subtle && (!expected || (await sha256(bytes)) !== expected)) return null
171
+ return bytes
172
+ } catch {
173
+ return null
174
+ }
175
+ }
176
+
177
+ async function remember(key, bytes) {
178
+ try {
179
+ if (!globalThis.caches) return
180
+ const headers = { 'content-type': 'application/wasm' }
181
+ if (globalThis.crypto?.subtle) headers['x-core-sha256'] = await sha256(bytes)
182
+ await (await caches.open(CACHE)).put(key, new Response(bytes, { headers }))
183
+ } catch {
184
+ // A full or unavailable cache only costs the next start a transpile.
185
+ }
186
+ }
187
+
188
+ /** The plugin's world core module and how it was obtained. */
189
+ async function mainModule(init, component) {
190
+ const entry = init.plugin
191
+ if (!component) {
192
+ const dir = new URL(`plugins/${entry.dir}/`, init.base)
193
+ return { bytes: await bundleFile(new URL(entry.core.file, dir).href, entry.core.sha256), source: 'bundle' }
194
+ }
195
+ // The viewer checked the package against its hash and hashed the
196
+ // component it extracted; the cache is keyed by that and by the glue.
197
+ const key = cacheKey(init.base, entry, init.world)
198
+ const hit = await cached(key)
199
+ if (hit) return { bytes: hit, source: 'cache' }
200
+ const { bytes, ms } = typeof Worker === 'function' ? await transpileElsewhere(component) : await transpileHere(component)
201
+ await remember(key, bytes)
202
+ return { bytes, source: 'transpiled', ms }
203
+ }
204
+
205
+ async function load(init, component) {
206
+ const started = performance.now()
207
+ memoryLimit = init.memoryLimit
208
+ const jspi = init.account?.mode === 'jspi'
209
+ const world = init.world
210
+ const glueFile = jspi ? world.jspi : world.glue
211
+ const [glue, shims, main] = await Promise.all([
212
+ import(new URL(`plugins/host/${glueFile.file}`, init.base).href),
213
+ Promise.all(world.shims.map(async shim => [shim.file, await WebAssembly.compile(
214
+ await bundleFile(new URL(`plugins/host/${shim.file}`, init.base).href, shim.sha256))])),
215
+ mainModule(init, component),
216
+ ])
217
+ const account = init.account
218
+ ? { mode: init.account.mode, ask, mirror: new Mirror(init.account.mirror ?? {}) }
219
+ : null
220
+ host = new PluginHost({
221
+ manifest: init.plugin.manifest,
222
+ grants: init.grants ?? undefined,
223
+ config: init.config ?? '{}',
224
+ view: init.view,
225
+ shared: init.shared,
226
+ post,
227
+ memory: () => memory(),
228
+ account,
229
+ deliver: event => enqueue({ t: 'event', event, gesture: false }),
230
+ })
231
+ if (init.accountState) host.accountState = init.accountState
232
+ const linked = instantiatePlugin({
233
+ glue,
234
+ shims: Object.fromEntries(shims),
235
+ main: await WebAssembly.compile(main.bytes),
236
+ imports: host.imports(),
237
+ // The surface's `attach` takes the region as a list<u8>; the canonical
238
+ // ABI hands the core import its address and length in linear memory,
239
+ // which the host records to copy committed slots out of later. The
240
+ // glue lifts only a copy of the bytes.
241
+ wrap: (module, name, fn) => (module === 'cm32p2|standard:plugin/surface@2' && name === 'attach'
242
+ ? (...args) => {
243
+ host.attaching = { ptr: args[2] >>> 0, len: args[3] >>> 0 }
244
+ return fn(...args)
245
+ }
246
+ : fn),
247
+ })
248
+ memory = linked.memory
249
+ plugin = linked.instance
250
+ const timing = { ms: performance.now() - started, source: main.source, transpile: main.ms ?? null }
251
+ enqueue({ t: 'activate', timing })
252
+ const waiting = early
253
+ early = []
254
+ for (const { message, buffers } of waiting) handle(message, buffers)
255
+ }
256
+
257
+ // ── calls into the plugin ───────────────────────────────────────────────────
258
+
259
+ function enqueue(message, buffers = []) {
260
+ queue.push({ message, buffers })
261
+ drain()
262
+ }
263
+
264
+ /** Runs queued work until the queue is empty or a call suspends. */
265
+ function drain() {
266
+ while (!running && !dead && plugin && queue.length) {
267
+ const { message, buffers } = queue.shift()
268
+ let result
269
+ calling = true
270
+ try {
271
+ result = begin(message, buffers)
272
+ } catch (error) {
273
+ calling = false
274
+ trapped(error)
275
+ return
276
+ }
277
+ if (result instanceof Promise) {
278
+ running = true
279
+ result.then((value) => {
280
+ running = false
281
+ calling = false
282
+ finish(message, value)
283
+ drain()
284
+ }, (error) => {
285
+ running = false
286
+ calling = false
287
+ trapped(error)
288
+ })
289
+ return
290
+ }
291
+ calling = false
292
+ finish(message, result)
293
+ }
294
+ }
295
+
296
+ /** Starts one piece of work; a call into the plugin may return a promise
297
+ * (JSPI). */
298
+ function begin(message, buffers) {
299
+ suspendedMs = 0
300
+ message.started = performance.now()
301
+ switch (message.t) {
302
+ case 'activate':
303
+ return plugin.activate(host.config)
304
+ case 'resize':
305
+ host.resize(message.surface, message.geometry, message.gen, buffers, message.model)
306
+ return plugin.event(witEvent({ tag: 'resize', surface: message.surface, geometry: message.geometry }))
307
+ case 'visibility':
308
+ return plugin.event(witEvent({ tag: 'visibility', surface: message.surface, visible: message.visible }))
309
+ case 'event':
310
+ // A key, paste, press or command lets the plugin open a surface
311
+ // while it handles it, and only then.
312
+ host.gesture = Boolean(message.gesture)
313
+ if (host.gesture) host.noteInput()
314
+ return plugin.event(witEvent(message.event))
315
+ case 'frame':
316
+ return plugin.frame(BigInt(message.now), message.interval, message.power)
317
+ case 'deactivate':
318
+ return plugin.deactivate()
319
+ default:
320
+ return undefined
321
+ }
322
+ }
323
+
324
+ function finish(message, result) {
325
+ host.gesture = false
326
+ if (dead || !checkMemory()) return
327
+ switch (message.t) {
328
+ case 'activate':
329
+ post({ t: 'ready', jspi: host.waits, timing: message.timing })
330
+ break
331
+ case 'frame': {
332
+ const elapsed = performance.now() - message.started - suspendedMs
333
+ post({ t: 'frame-done', next: result === undefined ? null : Number(result), elapsed })
334
+ break
335
+ }
336
+ case 'deactivate':
337
+ dead = true
338
+ self.close()
339
+ break
340
+ default:
341
+ break
342
+ }
343
+ }
344
+
345
+ // ── the viewer's messages ───────────────────────────────────────────────────
346
+
347
+ function handle(message, buffers) {
348
+ switch (message.t) {
349
+ case 'account-reply':
350
+ answered(message.id, message.reply)
351
+ break
352
+ case 'release':
353
+ if (!dead && host) host.release(message.surface, message.gen, message.slot, buffers[0])
354
+ break
355
+ case 'view':
356
+ if (host) host.view = message.view
357
+ break
358
+ case 'mirror':
359
+ host?.account?.mirror?.apply(message.values ?? {})
360
+ break
361
+ case 'account-state':
362
+ if (!host) break
363
+ host.accountState = message.state
364
+ if (host.accountWatched) enqueue({ t: 'event', event: { tag: 'account-changed' }, gesture: false })
365
+ break
366
+ case 'deliver':
367
+ enqueue({ t: 'event', event: message.event, gesture: false })
368
+ break
369
+ default:
370
+ enqueue(message, buffers)
371
+ }
372
+ }
373
+
374
+ async function transpileRole(component) {
375
+ try {
376
+ const { bytes, ms } = await transpileHere(component)
377
+ const buffer = bytes.slice().buffer
378
+ self.postMessage({ m: JSON.stringify({ t: 'transpiled', ms }), b: [buffer] }, [buffer])
379
+ } catch (error) {
380
+ self.postMessage({ m: JSON.stringify({ t: 'failed', error: describe(error) }) })
381
+ }
382
+ }
383
+
384
+ self.onmessage = (event) => {
385
+ const message = JSON.parse(event.data.m)
386
+ const buffers = event.data.b ?? []
387
+ if (message.t === 'transpile') {
388
+ transpileRole(new Uint8Array(buffers[0]))
389
+ return
390
+ }
391
+ if (message.t === 'init') {
392
+ epoch = message.epoch
393
+ // A Worker has no page address: resolve a relative bundle base against
394
+ // this script's own location, so callers may pass either form.
395
+ message.base = new URL(message.base, self.location.href).href
396
+ const component = buffers[0] ? new Uint8Array(buffers[0]) : null
397
+ load(message, component).catch((error) => {
398
+ dead = true
399
+ post({ t: 'failed', error: describe(error) })
400
+ })
401
+ return
402
+ }
403
+ // Until the plugin exists only queued work accumulates; view state and
404
+ // releases for it arrive only after its surfaces were sized.
405
+ if (!host && message.t !== 'account-reply') {
406
+ early.push({ message, buffers })
407
+ return
408
+ }
409
+ handle(message, buffers)
410
+ }
411
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@standardagents/code-web",
3
- "version": "0.14.1-issue.663.fd4c3f",
3
+ "version": "0.14.4-branch.9e48e914.ee06ef",
4
4
  "type": "module",
5
5
  "description": "The Standard viewer runtime for the browser: the TUI compiled to wasm, its JS glue, and its fonts",
6
6
  "license": "UNLICENSED",
@@ -27,6 +27,7 @@
27
27
  "./standard_web_bg.wasm": "./dist/pkg/standard_web_bg.wasm",
28
28
  "./fonts/*": "./dist/fonts/*",
29
29
  "./licenses/*": "./dist/licenses/*",
30
+ "./plugins/*": "./dist/plugins/*",
30
31
  "./build.json": "./dist/build.json",
31
32
  "./manifest.json": "./dist/manifest.json",
32
33
  "./manifest.sig": "./dist/manifest.sig",
@@ -36,6 +37,7 @@
36
37
  "dist/pkg",
37
38
  "dist/fonts",
38
39
  "dist/licenses",
40
+ "dist/plugins",
39
41
  "dist/build.json",
40
42
  "dist/manifest.json",
41
43
  "dist/manifest.sig",
@@ -45,6 +47,10 @@
45
47
  "fonts": "node scripts/fetch-fonts.mjs",
46
48
  "build": "node scripts/build.mjs",
47
49
  "check": "node scripts/build.mjs --check",
48
- "serve": "node scripts/serve.mjs"
50
+ "serve": "node scripts/serve.mjs",
51
+ "test": "node --test test/*.test.mjs"
52
+ },
53
+ "devDependencies": {
54
+ "@bytecodealliance/jco-transpile": "0.15.0"
49
55
  }
50
56
  }