@crowdedkingdoms/crowdyjs 8.21.1 → 9.0.0

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 (55) hide show
  1. package/MIGRATION.md +44 -0
  2. package/README.md +74 -3
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/live-coding/assets/browser-authoring-index.json +1 -0
  7. package/dist/live-coding/assets/manifest.json +19 -0
  8. package/dist/live-coding/assets/tree-sitter-rust.wasm +0 -0
  9. package/dist/live-coding/assets/web-tree-sitter.wasm +0 -0
  10. package/dist/live-coding/browser-authoring-index.generated.d.ts +2 -0
  11. package/dist/live-coding/browser-authoring-index.generated.d.ts.map +1 -0
  12. package/dist/live-coding/browser-authoring-index.generated.js +5127 -0
  13. package/dist/live-coding/ide.d.ts +14 -7
  14. package/dist/live-coding/ide.d.ts.map +1 -1
  15. package/dist/live-coding/ide.js +296 -181
  16. package/dist/live-coding/index.d.ts +8 -0
  17. package/dist/live-coding/index.d.ts.map +1 -0
  18. package/dist/live-coding/index.js +7 -0
  19. package/dist/live-coding/lsp-protocol.d.ts +98 -0
  20. package/dist/live-coding/lsp-protocol.d.ts.map +1 -0
  21. package/dist/live-coding/lsp-protocol.js +70 -0
  22. package/dist/live-coding/monaco-services.d.ts +22 -0
  23. package/dist/live-coding/monaco-services.d.ts.map +1 -0
  24. package/dist/live-coding/monaco-services.js +69 -0
  25. package/dist/live-coding/platform-index.d.ts +29 -0
  26. package/dist/live-coding/platform-index.d.ts.map +1 -0
  27. package/dist/live-coding/platform-index.js +281 -0
  28. package/dist/live-coding/rust-analysis.d.ts +24 -0
  29. package/dist/live-coding/rust-analysis.d.ts.map +1 -0
  30. package/dist/live-coding/rust-analysis.js +312 -0
  31. package/dist/live-coding/rust-lsp-server.d.ts +43 -0
  32. package/dist/live-coding/rust-lsp-server.d.ts.map +1 -0
  33. package/dist/live-coding/rust-lsp-server.js +455 -0
  34. package/dist/live-coding/rust-lsp.worker.d.ts +2 -0
  35. package/dist/live-coding/rust-lsp.worker.d.ts.map +1 -0
  36. package/dist/live-coding/rust-lsp.worker.js +26 -0
  37. package/dist/live-coding/vfs.d.ts +41 -0
  38. package/dist/live-coding/vfs.d.ts.map +1 -0
  39. package/dist/live-coding/vfs.js +174 -0
  40. package/dist/live-coding/worker-transport.d.ts +60 -0
  41. package/dist/live-coding/worker-transport.d.ts.map +1 -0
  42. package/dist/live-coding/worker-transport.js +204 -0
  43. package/dist/player-runtime/glue-runtime.d.ts +13 -4
  44. package/dist/player-runtime/glue-runtime.d.ts.map +1 -1
  45. package/dist/player-runtime/glue-runtime.js +52 -8
  46. package/dist/player-runtime/glue-sab.d.ts +8 -5
  47. package/dist/player-runtime/glue-sab.d.ts.map +1 -1
  48. package/dist/player-runtime/glue-sab.js +36 -10
  49. package/dist/player-runtime/player-code-broker.d.ts +33 -4
  50. package/dist/player-runtime/player-code-broker.d.ts.map +1 -1
  51. package/dist/player-runtime/player-code-broker.js +332 -51
  52. package/dist/player-runtime/player-glue-worker.d.ts +3 -2
  53. package/dist/player-runtime/player-glue-worker.d.ts.map +1 -1
  54. package/dist/player-runtime/player-glue-worker.js +68 -32
  55. package/package.json +18 -6
@@ -0,0 +1,174 @@
1
+ export const DEFAULT_VFS_LIMITS = {
2
+ maxFiles: 32,
3
+ maxFileBytes: 256 * 1024,
4
+ maxWorkspaceBytes: 1024 * 1024,
5
+ };
6
+ export class VfsLimitError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.code = 'VFS_LIMIT';
10
+ this.name = 'VfsLimitError';
11
+ }
12
+ }
13
+ export class VirtualFileSystem {
14
+ constructor(workspaceUri = 'file:///player-mod', limits = {}) {
15
+ this.files = new Map();
16
+ this.encoder = new TextEncoder();
17
+ this.totalBytes = 0;
18
+ this.workspaceUri = workspaceUri.replace(/\/+$/, '');
19
+ const root = new URL(this.workspaceUri);
20
+ if (root.protocol !== 'file:' ||
21
+ root.search ||
22
+ root.hash ||
23
+ !root.pathname.startsWith('/') ||
24
+ root.pathname === '/') {
25
+ throw new Error('workspaceUri must be a non-root file URI');
26
+ }
27
+ this.limits = { ...DEFAULT_VFS_LIMITS, ...limits };
28
+ this.assertLimits();
29
+ }
30
+ get size() {
31
+ return this.files.size;
32
+ }
33
+ get bytes() {
34
+ return this.totalBytes;
35
+ }
36
+ open(item) {
37
+ const path = this.pathForUri(item.uri);
38
+ const previous = this.files.get(item.uri);
39
+ if (!previous && this.files.size >= this.limits.maxFiles) {
40
+ throw new VfsLimitError(`Workspace file limit is ${this.limits.maxFiles}`);
41
+ }
42
+ if (previous && item.version <= previous.version) {
43
+ return previous;
44
+ }
45
+ const next = this.makeDocument(item, path);
46
+ this.ensureFits(next.bytes, previous?.bytes ?? 0);
47
+ if (previous)
48
+ this.totalBytes -= previous.bytes;
49
+ this.files.set(item.uri, next);
50
+ this.totalBytes += next.bytes;
51
+ return next;
52
+ }
53
+ change(uri, version, changes) {
54
+ const previous = this.require(uri);
55
+ if (version <= previous.version) {
56
+ return { applied: false, document: previous };
57
+ }
58
+ let text = previous.text;
59
+ for (const change of changes) {
60
+ if (!change.range) {
61
+ text = change.text;
62
+ continue;
63
+ }
64
+ const start = offsetAt(text, change.range.start);
65
+ const end = offsetAt(text, change.range.end);
66
+ if (end < start)
67
+ throw new Error('Invalid content change range');
68
+ text = text.slice(0, start) + change.text + text.slice(end);
69
+ }
70
+ const next = this.makeDocument({
71
+ uri,
72
+ version,
73
+ languageId: previous.languageId,
74
+ text,
75
+ }, previous.path);
76
+ this.ensureFits(next.bytes, previous.bytes);
77
+ this.files.set(uri, next);
78
+ this.totalBytes += next.bytes - previous.bytes;
79
+ return { applied: true, document: next };
80
+ }
81
+ close(uri) {
82
+ const previous = this.files.get(uri);
83
+ if (!previous)
84
+ return false;
85
+ this.files.delete(uri);
86
+ this.totalBytes -= previous.bytes;
87
+ return true;
88
+ }
89
+ get(uri) {
90
+ return this.files.get(uri);
91
+ }
92
+ require(uri) {
93
+ const document = this.files.get(uri);
94
+ if (!document)
95
+ throw new Error(`Document is not open: ${uri}`);
96
+ return document;
97
+ }
98
+ documents() {
99
+ return [...this.files.values()];
100
+ }
101
+ clear() {
102
+ this.files.clear();
103
+ this.totalBytes = 0;
104
+ }
105
+ pathForUri(uri) {
106
+ let parsed;
107
+ try {
108
+ parsed = new URL(uri);
109
+ }
110
+ catch {
111
+ throw new Error(`Invalid document URI: ${uri}`);
112
+ }
113
+ if (parsed.protocol !== 'file:' ||
114
+ parsed.search ||
115
+ parsed.hash ||
116
+ !uri.startsWith(`${this.workspaceUri}/`)) {
117
+ throw new Error(`Document is outside ${this.workspaceUri}`);
118
+ }
119
+ const relative = decodeURIComponent(parsed.pathname.slice(new URL(this.workspaceUri).pathname.length + 1));
120
+ if (relative.length === 0 ||
121
+ relative.startsWith('/') ||
122
+ relative.split('/').some((part) => part === '' || part === '..' || part === '.')) {
123
+ throw new Error(`Unsafe document path: ${relative}`);
124
+ }
125
+ return relative;
126
+ }
127
+ makeDocument(item, path) {
128
+ if (!Number.isSafeInteger(item.version) || item.version < 0) {
129
+ throw new Error('Document version must be a non-negative safe integer');
130
+ }
131
+ const bytes = this.encoder.encode(item.text).byteLength;
132
+ if (bytes > this.limits.maxFileBytes) {
133
+ throw new VfsLimitError(`${path} is ${bytes} bytes; file limit is ${this.limits.maxFileBytes}`);
134
+ }
135
+ return Object.freeze({ ...item, path, bytes });
136
+ }
137
+ ensureFits(nextBytes, replacedBytes) {
138
+ const total = this.totalBytes - replacedBytes + nextBytes;
139
+ if (total > this.limits.maxWorkspaceBytes) {
140
+ throw new VfsLimitError(`Workspace is ${total} bytes; limit is ${this.limits.maxWorkspaceBytes}`);
141
+ }
142
+ }
143
+ assertLimits() {
144
+ for (const [name, value] of Object.entries(this.limits)) {
145
+ if (!Number.isSafeInteger(value) || value <= 0) {
146
+ throw new Error(`${name} must be a positive safe integer`);
147
+ }
148
+ }
149
+ if (this.limits.maxFileBytes > this.limits.maxWorkspaceBytes) {
150
+ throw new Error('maxFileBytes cannot exceed maxWorkspaceBytes');
151
+ }
152
+ }
153
+ }
154
+ export function offsetAt(text, position) {
155
+ if (!Number.isSafeInteger(position.line) ||
156
+ !Number.isSafeInteger(position.character) ||
157
+ position.line < 0 ||
158
+ position.character < 0) {
159
+ throw new Error('Invalid document position');
160
+ }
161
+ let offset = 0;
162
+ for (let line = 0; line < position.line; line++) {
163
+ const newline = text.indexOf('\n', offset);
164
+ if (newline < 0)
165
+ throw new Error('Document position is outside the text');
166
+ offset = newline + 1;
167
+ }
168
+ const newline = text.indexOf('\n', offset);
169
+ const lineEnd = newline < 0 ? text.length : newline;
170
+ const target = offset + position.character;
171
+ if (target > lineEnd)
172
+ throw new Error('Document position is outside the text');
173
+ return target;
174
+ }
@@ -0,0 +1,60 @@
1
+ import { AbstractMessageReader, AbstractMessageWriter, type DataCallback, type Disposable, type Message } from 'vscode-jsonrpc';
2
+ import { type JsonRpcMessage } from './lsp-protocol.js';
3
+ export type { Disposable };
4
+ export interface LanguageWorkerLike {
5
+ postMessage(message: unknown): void;
6
+ addEventListener(type: 'message' | 'error' | 'messageerror', listener: EventListener): void;
7
+ removeEventListener(type: 'message' | 'error' | 'messageerror', listener: EventListener): void;
8
+ terminate(): void;
9
+ }
10
+ /** Standard vscode-jsonrpc MessageReader over structured-clone worker messages. */
11
+ export declare class WorkerMessageReader extends AbstractMessageReader {
12
+ private readonly worker;
13
+ private callback;
14
+ private listening;
15
+ constructor(worker: LanguageWorkerLike);
16
+ listen(callback: DataCallback): Disposable;
17
+ dispose(): void;
18
+ private readonly onMessage;
19
+ private readonly onMessageError;
20
+ private readonly onWorkerError;
21
+ }
22
+ /** Standard vscode-jsonrpc MessageWriter over structured-clone worker messages. */
23
+ export declare class WorkerMessageWriter extends AbstractMessageWriter {
24
+ private readonly worker;
25
+ private disposed;
26
+ private errorCount;
27
+ constructor(worker: LanguageWorkerLike);
28
+ write(message: Message): Promise<void>;
29
+ end(): void;
30
+ dispose(): void;
31
+ }
32
+ export interface WorkerLanguageClientOptions {
33
+ requestTimeoutMs?: number;
34
+ }
35
+ /**
36
+ * LSP 3.17 JSON-RPC client over a real vscode-jsonrpc MessageConnection.
37
+ * Monaco providers adapt editor calls to this connection; transport framing,
38
+ * request ids, cancellation, errors, and notifications are standard JSON-RPC.
39
+ */
40
+ export declare class WorkerLanguageClient {
41
+ private readonly worker;
42
+ private readonly options;
43
+ private readonly reader;
44
+ private readonly writer;
45
+ private readonly connection;
46
+ private readonly transportErrorHandlers;
47
+ private readonly readerErrorDisposable;
48
+ private disposed;
49
+ private terminateTimer;
50
+ constructor(worker: LanguageWorkerLike, options?: WorkerLanguageClientOptions);
51
+ initialize(params: unknown): Promise<unknown>;
52
+ request(method: string, params?: unknown): Promise<unknown>;
53
+ notify(method: string, params?: unknown): Promise<void>;
54
+ onNotification(method: string, callback: (params: unknown) => void): Disposable;
55
+ shutdown(): void;
56
+ dispose(): void;
57
+ }
58
+ export declare function createDefaultRustLanguageWorker(): Worker;
59
+ export declare function isWorkerLspMessage(value: unknown): value is JsonRpcMessage;
60
+ //# sourceMappingURL=worker-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-transport.d.ts","sourceRoot":"","sources":["../../src/live-coding/worker-transport.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EAErB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,OAAO,EACb,MAAM,gBAAgB,CAAC;AASxB,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EAAE,UAAU,EAAE,CAAC;AAE3B,MAAM,WAAW,kBAAkB;IACjC,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACpC,gBAAgB,CACd,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,cAAc,EAC1C,QAAQ,EAAE,aAAa,GACtB,IAAI,CAAC;IACR,mBAAmB,CACjB,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,cAAc,EAC1C,QAAQ,EAAE,aAAa,GACtB,IAAI,CAAC;IACR,SAAS,IAAI,IAAI,CAAC;CACnB;AAED,mFAAmF;AACnF,qBAAa,mBAAoB,SAAQ,qBAAqB;IAIhD,OAAO,CAAC,QAAQ,CAAC,MAAM;IAHnC,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,SAAS,CAAS;gBAEG,MAAM,EAAE,kBAAkB;IAIvD,MAAM,CAAC,QAAQ,EAAE,YAAY,GAAG,UAAU;IAUjC,OAAO,IAAI,IAAI;IAWxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAOxB;IAEF,OAAO,CAAC,QAAQ,CAAC,cAAc,CAE7B;IAEF,OAAO,CAAC,QAAQ,CAAC,aAAa,CAM5B;CACH;AAED,mFAAmF;AACnF,qBAAa,mBAAoB,SAAQ,qBAAqB;IAIhD,OAAO,CAAC,QAAQ,CAAC,MAAM;IAHnC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAK;gBAEM,MAAM,EAAE,kBAAkB;IAIvD,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAYtC,GAAG,IAAI,IAAI;IAIF,OAAO,IAAI,IAAI;CAIzB;AAED,MAAM,WAAW,2BAA2B;IAC1C,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,qBAAa,oBAAoB;IAU7B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAV1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAqC;IAC5E,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAa;IACnD,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAA8C;gBAGjD,MAAM,EAAE,kBAAkB,EAC1B,OAAO,GAAE,2BAAgC;IAWtD,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAMnD,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAsD3D,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAKvD,cAAc,CACZ,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,GAClC,UAAU;IAIb,QAAQ,IAAI,IAAI;IAehB,OAAO,IAAI,IAAI;CAYhB;AAED,wBAAgB,+BAA+B,IAAI,MAAM,CAKxD;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAE1E"}
@@ -0,0 +1,204 @@
1
+ import { AbstractMessageReader, AbstractMessageWriter, CancellationTokenSource, } from 'vscode-jsonrpc';
2
+ import { ExitNotification, InitializeRequest, InitializedNotification, ShutdownRequest, createProtocolConnection, } from 'vscode-languageserver-protocol';
3
+ import { decodeJsonRpcMessage, } from './lsp-protocol.js';
4
+ /** Standard vscode-jsonrpc MessageReader over structured-clone worker messages. */
5
+ export class WorkerMessageReader extends AbstractMessageReader {
6
+ constructor(worker) {
7
+ super();
8
+ this.worker = worker;
9
+ this.callback = null;
10
+ this.listening = false;
11
+ this.onMessage = (event) => {
12
+ const decoded = decodeJsonRpcMessage(event.data);
13
+ if (!decoded.ok) {
14
+ this.fireError(new Error(decoded.message));
15
+ return;
16
+ }
17
+ this.callback?.(decoded.message);
18
+ };
19
+ this.onMessageError = () => {
20
+ this.fireError(new Error('Language worker sent an unreadable message'));
21
+ };
22
+ this.onWorkerError = (event) => {
23
+ const message = 'message' in event && typeof event.message === 'string'
24
+ ? event.message
25
+ : 'Language worker failed';
26
+ this.fireError(new Error(message));
27
+ };
28
+ }
29
+ listen(callback) {
30
+ if (this.listening)
31
+ throw new Error('WorkerMessageReader is already listening');
32
+ this.listening = true;
33
+ this.callback = callback;
34
+ this.worker.addEventListener('message', this.onMessage);
35
+ this.worker.addEventListener('messageerror', this.onMessageError);
36
+ this.worker.addEventListener('error', this.onWorkerError);
37
+ return { dispose: () => this.dispose() };
38
+ }
39
+ dispose() {
40
+ if (this.listening) {
41
+ this.worker.removeEventListener('message', this.onMessage);
42
+ this.worker.removeEventListener('messageerror', this.onMessageError);
43
+ this.worker.removeEventListener('error', this.onWorkerError);
44
+ }
45
+ this.listening = false;
46
+ this.callback = null;
47
+ super.dispose();
48
+ }
49
+ }
50
+ /** Standard vscode-jsonrpc MessageWriter over structured-clone worker messages. */
51
+ export class WorkerMessageWriter extends AbstractMessageWriter {
52
+ constructor(worker) {
53
+ super();
54
+ this.worker = worker;
55
+ this.disposed = false;
56
+ this.errorCount = 0;
57
+ }
58
+ write(message) {
59
+ if (this.disposed)
60
+ return Promise.reject(new Error('Worker writer is disposed'));
61
+ try {
62
+ this.worker.postMessage(message);
63
+ return Promise.resolve();
64
+ }
65
+ catch (error) {
66
+ this.errorCount++;
67
+ this.fireError(error, message, this.errorCount);
68
+ return Promise.reject(error);
69
+ }
70
+ }
71
+ end() {
72
+ this.dispose();
73
+ }
74
+ dispose() {
75
+ this.disposed = true;
76
+ super.dispose();
77
+ }
78
+ }
79
+ /**
80
+ * LSP 3.17 JSON-RPC client over a real vscode-jsonrpc MessageConnection.
81
+ * Monaco providers adapt editor calls to this connection; transport framing,
82
+ * request ids, cancellation, errors, and notifications are standard JSON-RPC.
83
+ */
84
+ export class WorkerLanguageClient {
85
+ constructor(worker, options = {}) {
86
+ this.worker = worker;
87
+ this.options = options;
88
+ this.transportErrorHandlers = new Set();
89
+ this.disposed = false;
90
+ this.terminateTimer = null;
91
+ this.reader = new WorkerMessageReader(worker);
92
+ this.writer = new WorkerMessageWriter(worker);
93
+ this.readerErrorDisposable = this.reader.onError((error) => {
94
+ for (const handler of this.transportErrorHandlers)
95
+ handler(error);
96
+ });
97
+ this.connection = createProtocolConnection(this.reader, this.writer);
98
+ this.connection.listen();
99
+ }
100
+ async initialize(params) {
101
+ const result = await this.request(InitializeRequest.type.method, params);
102
+ await this.notify(InitializedNotification.type.method, {});
103
+ return result;
104
+ }
105
+ request(method, params) {
106
+ if (this.disposed)
107
+ return Promise.reject(new Error('Language client is disposed'));
108
+ const cancellation = new CancellationTokenSource();
109
+ const timeoutMs = this.options.requestTimeoutMs ?? 3000;
110
+ return new Promise((resolve, reject) => {
111
+ let settled = false;
112
+ const onTransportError = (error) => {
113
+ if (settled)
114
+ return;
115
+ settled = true;
116
+ clearTimeout(timer);
117
+ cancellation.cancel();
118
+ cancellation.dispose();
119
+ this.transportErrorHandlers.delete(onTransportError);
120
+ reject(error);
121
+ };
122
+ const timer = setTimeout(() => {
123
+ if (settled)
124
+ return;
125
+ settled = true;
126
+ cancellation.cancel();
127
+ cancellation.dispose();
128
+ this.transportErrorHandlers.delete(onTransportError);
129
+ reject(new Error(`Language request timed out: ${method}`));
130
+ }, timeoutMs);
131
+ this.transportErrorHandlers.add(onTransportError);
132
+ const request = params === undefined
133
+ ? this.connection.sendRequest(method, cancellation.token)
134
+ : this.connection.sendRequest(method, params, cancellation.token);
135
+ void request
136
+ .then((value) => {
137
+ if (settled)
138
+ return;
139
+ settled = true;
140
+ clearTimeout(timer);
141
+ cancellation.dispose();
142
+ this.transportErrorHandlers.delete(onTransportError);
143
+ resolve(value);
144
+ }, (error) => {
145
+ if (settled)
146
+ return;
147
+ settled = true;
148
+ clearTimeout(timer);
149
+ cancellation.dispose();
150
+ this.transportErrorHandlers.delete(onTransportError);
151
+ reject(error instanceof Error
152
+ ? error
153
+ : new Error('Language request failed'));
154
+ });
155
+ });
156
+ }
157
+ notify(method, params) {
158
+ if (this.disposed)
159
+ return Promise.reject(new Error('Language client is disposed'));
160
+ return this.connection.sendNotification(method, params);
161
+ }
162
+ onNotification(method, callback) {
163
+ return this.connection.onNotification(method, callback);
164
+ }
165
+ shutdown() {
166
+ if (this.disposed || this.terminateTimer)
167
+ return;
168
+ const finish = () => {
169
+ if (this.disposed)
170
+ return;
171
+ void this.connection
172
+ .sendNotification(ExitNotification.type.method)
173
+ .catch(() => { })
174
+ .finally(() => this.dispose());
175
+ };
176
+ void this.request(ShutdownRequest.type.method)
177
+ .catch(() => { })
178
+ .finally(finish);
179
+ this.terminateTimer = setTimeout(finish, this.options.requestTimeoutMs ?? 3000);
180
+ }
181
+ dispose() {
182
+ if (this.disposed)
183
+ return;
184
+ this.disposed = true;
185
+ if (this.terminateTimer)
186
+ clearTimeout(this.terminateTimer);
187
+ this.terminateTimer = null;
188
+ this.transportErrorHandlers.clear();
189
+ this.readerErrorDisposable.dispose();
190
+ this.connection.dispose();
191
+ this.reader.dispose();
192
+ this.writer.dispose();
193
+ this.worker.terminate();
194
+ }
195
+ }
196
+ export function createDefaultRustLanguageWorker() {
197
+ return new Worker(new URL('./rust-lsp.worker.js', import.meta.url), {
198
+ type: 'module',
199
+ name: 'crowdy-rust-lsp',
200
+ });
201
+ }
202
+ export function isWorkerLspMessage(value) {
203
+ return decodeJsonRpcMessage(value).ok;
204
+ }
@@ -25,12 +25,18 @@ export interface GlueInitMessage {
25
25
  type: 'init';
26
26
  artifact: ArrayBuffer;
27
27
  authority: 'player';
28
+ /** Server-authored budget loaded into an injected mutable `ck_fuel` global. */
28
29
  fuelPerDispatch?: string;
30
+ /** Legacy metadata; the hard watchdog is owned by the page-side broker. */
29
31
  watchdogMs?: number;
32
+ hostCallTimeoutMs?: number;
30
33
  /** Local client tick cadence in ms (0/undefined => no self-tick). */
31
34
  tickIntervalMs?: number;
32
35
  }
33
- /** Parse the fuel budget the broker forwards; undefined/invalid => unbounded (server still meters). */
36
+ /**
37
+ * Parse the server-authored budget used to refill an instrumented artifact's
38
+ * mutable `ck_fuel` global before every guest dispatch.
39
+ */
34
40
  export declare function parseFuelBudget(raw: string | undefined): bigint | null;
35
41
  /** A dispatch outcome the worker reports back to the broker. */
36
42
  export type GlueDispatchResult = {
@@ -41,9 +47,9 @@ export type GlueDispatchResult = {
41
47
  detail?: string;
42
48
  };
43
49
  /**
44
- * Wrap a single guest dispatch with the wall-clock watchdog. The fuel trap is
45
- * enforced inside the gas-injected module; this guards against a hang that
46
- * spins without consuming fuel. Pure and unit-testable.
50
+ * Classify a completed guest dispatch by elapsed wall time. This cannot
51
+ * interrupt synchronous WASM; the page-side PlayerCodeBroker owns the hard
52
+ * dispatch watchdog and terminates a worker whose dispatch never returns.
47
53
  */
48
54
  export declare function runWithWatchdog(dispatch: () => unknown, watchdogMs: number, now?: () => number): Promise<GlueDispatchResult>;
49
55
  /** The minimal guest-instance surface the runtime drives (a real WebAssembly.Instance satisfies it). */
@@ -51,6 +57,7 @@ export interface GuestExports {
51
57
  memory: {
52
58
  buffer: ArrayBuffer;
53
59
  };
60
+ ck_fuel?: WebAssembly.Global;
54
61
  ck_alloc(len: number): number;
55
62
  ck_free?(ptr: number, len: number): void;
56
63
  init?(): void;
@@ -66,6 +73,7 @@ export interface GlueRuntimeOptions {
66
73
  /** Deterministic-enough randomness for the guest `random_get` (defaults to crypto). */
67
74
  randomFill?: (buf: Uint8Array) => void;
68
75
  now?: () => number;
76
+ fuelPerDispatch?: bigint | null;
69
77
  }
70
78
  /**
71
79
  * Drives one untrusted guest module: builds the `ck` + wasi import table,
@@ -79,6 +87,7 @@ export declare class GlueRuntime {
79
87
  private exports;
80
88
  private stateBlob;
81
89
  constructor(options: GlueRuntimeOptions);
90
+ private resetFuel;
82
91
  /** The import object handed to `WebAssembly.instantiate`. Guest sees only these. */
83
92
  buildImports(getExports: () => GuestExports | null): WebAssembly.Imports;
84
93
  instantiate(artifact: ArrayBuffer): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"glue-runtime.d.ts","sourceRoot":"","sources":["../../src/player-runtime/glue-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,mBAAmB,iVAoBtB,CAAC;AAEX,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,WAAW,CAAC;IACtB,SAAS,EAAE,QAAQ,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,uGAAuG;AACvG,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAQtE;AAED,gEAAgE;AAChE,MAAM,MAAM,kBAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzE;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,OAAO,EACvB,UAAU,EAAE,MAAM,EAClB,GAAG,GAAE,MAAM,MAAyB,GACnC,OAAO,CAAC,kBAAkB,CAAC,CAe7B;AAED,wGAAwG;AACxG,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE;QAAE,MAAM,EAAE,WAAW,CAAA;KAAE,CAAC;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,IAAI,CAAC,IAAI,IAAI,CAAC;IACd,IAAI,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IAC1D,QAAQ,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3C;AAED,MAAM,WAAW,kBAAkB;IACjC,4FAA4F;IAC5F,YAAY,EAAE,CAAC,QAAQ,EAAE,UAAU,KAAK,UAAU,CAAC;IACnD,gEAAgE;IAChE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,uFAAuF;IACvF,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,IAAI,CAAC;IACvC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAgBD;;;;;;GAMG;AACH,qBAAa,WAAW;IAIV,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,SAAS,CAAiC;gBAErB,OAAO,EAAE,kBAAkB;IAExD,oFAAoF;IACpF,YAAY,CAAC,UAAU,EAAE,MAAM,YAAY,GAAG,IAAI,GAAG,WAAW,CAAC,OAAO;IAkFlE,WAAW,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAUvD,gEAAgE;IAChE,IAAI,IAAI,IAAI;IAIZ,gFAAgF;IAChF,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIxB,sFAAsF;IACtF,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,UAAU;CAcxC"}
1
+ {"version":3,"file":"glue-runtime.d.ts","sourceRoot":"","sources":["../../src/player-runtime/glue-runtime.ts"],"names":[],"mappings":"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"}
@@ -41,7 +41,10 @@ export const GLUE_HOST_FUNCTIONS = [
41
41
  'overlay_draw',
42
42
  'grid_permission_check',
43
43
  ];
44
- /** Parse the fuel budget the broker forwards; undefined/invalid => unbounded (server still meters). */
44
+ /**
45
+ * Parse the server-authored budget used to refill an instrumented artifact's
46
+ * mutable `ck_fuel` global before every guest dispatch.
47
+ */
45
48
  export function parseFuelBudget(raw) {
46
49
  if (raw == null)
47
50
  return null;
@@ -54,9 +57,9 @@ export function parseFuelBudget(raw) {
54
57
  }
55
58
  }
56
59
  /**
57
- * Wrap a single guest dispatch with the wall-clock watchdog. The fuel trap is
58
- * enforced inside the gas-injected module; this guards against a hang that
59
- * spins without consuming fuel. Pure and unit-testable.
60
+ * Classify a completed guest dispatch by elapsed wall time. This cannot
61
+ * interrupt synchronous WASM; the page-side PlayerCodeBroker owns the hard
62
+ * dispatch watchdog and terminates a worker whose dispatch never returns.
60
63
  */
61
64
  export async function runWithWatchdog(dispatch, watchdogMs, now = () => Date.now()) {
62
65
  const start = now();
@@ -76,6 +79,16 @@ export async function runWithWatchdog(dispatch, watchdogMs, now = () => Date.now
76
79
  return { ok: true };
77
80
  }
78
81
  const textDecoder = new TextDecoder();
82
+ function assertMemoryRange(buffer, ptr, len, operation) {
83
+ if (!Number.isSafeInteger(ptr) ||
84
+ !Number.isSafeInteger(len) ||
85
+ ptr < 0 ||
86
+ len < 0 ||
87
+ ptr > buffer.byteLength ||
88
+ len > buffer.byteLength - ptr) {
89
+ throw new RangeError(`${operation} is outside guest memory`);
90
+ }
91
+ }
79
92
  function defaultRandomFill(buf) {
80
93
  const c = globalThis.crypto;
81
94
  if (c?.getRandomValues) {
@@ -102,6 +115,15 @@ export class GlueRuntime {
102
115
  this.exports = null;
103
116
  this.stateBlob = new Uint8Array(0);
104
117
  }
118
+ resetFuel() {
119
+ const fuel = this.exports?.ck_fuel;
120
+ if (!fuel)
121
+ return;
122
+ if (this.options.fuelPerDispatch == null) {
123
+ throw new Error('instrumented artifact is missing a fuel budget');
124
+ }
125
+ fuel.value = this.options.fuelPerDispatch;
126
+ }
105
127
  /** The import object handed to `WebAssembly.instantiate`. Guest sees only these. */
106
128
  buildImports(getExports) {
107
129
  const mem = () => {
@@ -114,17 +136,21 @@ export class GlueRuntime {
114
136
  const ex = getExports();
115
137
  if (!ex)
116
138
  throw new Error('guest not instantiated');
139
+ const buffer = ex.memory.buffer;
140
+ assertMemoryRange(buffer, ptr, len, 'guest memory read');
117
141
  // Copy into a fresh ArrayBuffer-backed view — the guest buffer may
118
142
  // detach/grow between calls (and may be a SharedArrayBuffer).
119
143
  const out = new Uint8Array(len);
120
- out.set(new Uint8Array(ex.memory.buffer, ptr, len));
144
+ out.set(new Uint8Array(buffer, ptr, len));
121
145
  return out;
122
146
  };
123
147
  const writeAt = (ptr, src) => {
124
148
  const ex = getExports();
125
149
  if (!ex)
126
150
  throw new Error('guest not instantiated');
127
- new Uint8Array(ex.memory.buffer, ptr, src.length).set(src);
151
+ const buffer = ex.memory.buffer;
152
+ assertMemoryRange(buffer, ptr, src.length, 'guest memory write');
153
+ new Uint8Array(buffer, ptr, src.length).set(src);
128
154
  };
129
155
  const now = this.options.now ?? (() => Date.now());
130
156
  const randomFill = this.options.randomFill ?? defaultRandomFill;
@@ -153,6 +179,9 @@ export class GlueRuntime {
153
179
  if (!ex)
154
180
  throw new Error('guest not instantiated');
155
181
  const outPtr = ex.ck_alloc(respBytes.length);
182
+ if (respBytes.length > 0 && outPtr === 0) {
183
+ throw new RangeError('ck_alloc returned a null reply pointer');
184
+ }
156
185
  writeAt(outPtr, respBytes);
157
186
  // Packed (ptr << 32 | len); the guest reads then ck_frees it.
158
187
  return (BigInt(outPtr) << 32n) | BigInt(respBytes.length >>> 0);
@@ -160,9 +189,13 @@ export class GlueRuntime {
160
189
  };
161
190
  const wasi = {
162
191
  random_get: (ptr, len) => {
163
- const buf = new Uint8Array(len);
192
+ const ex = getExports();
193
+ if (!ex)
194
+ throw new Error('guest not instantiated');
195
+ const buffer = ex.memory.buffer;
196
+ assertMemoryRange(buffer, ptr, len, 'random_get write');
197
+ const buf = new Uint8Array(buffer, ptr, len);
164
198
  randomFill(buf);
165
- writeAt(ptr, buf);
166
199
  return 0;
167
200
  },
168
201
  // A player artifact may pull in a few benign wasi stubs; keep them inert.
@@ -196,10 +229,12 @@ export class GlueRuntime {
196
229
  }
197
230
  /** Run the module's `init` export (once, after instantiate). */
198
231
  init() {
232
+ this.resetFuel();
199
233
  this.exports?.init?.();
200
234
  }
201
235
  /** Run one `tick(dt_ms)`. Throws propagate to the caller's watchdog wrapper. */
202
236
  tick(dtMs) {
237
+ this.resetFuel();
203
238
  this.exports?.tick?.(dtMs);
204
239
  }
205
240
  /** Invoke the module with an opaque payload; returns the reply bytes (copied out). */
@@ -208,13 +243,22 @@ export class GlueRuntime {
208
243
  if (!ex || typeof ex.handle_invoke !== 'function')
209
244
  return new Uint8Array(0);
210
245
  const ptr = ex.ck_alloc(payload.length);
246
+ if (payload.length > 0 && ptr === 0) {
247
+ throw new RangeError('ck_alloc returned a null invoke pointer');
248
+ }
249
+ assertMemoryRange(ex.memory.buffer, ptr, payload.length, 'invoke request write');
211
250
  new Uint8Array(ex.memory.buffer, ptr, payload.length).set(payload);
251
+ this.resetFuel();
212
252
  const packed = BigInt(ex.handle_invoke(ptr, payload.length));
213
253
  ex.ck_free?.(ptr, payload.length);
214
254
  const outPtr = Number(packed >> 32n);
215
255
  const outLen = Number(packed & 0xffffffffn);
216
256
  if (outLen === 0)
217
257
  return new Uint8Array(0);
258
+ if (outPtr === 0) {
259
+ throw new RangeError('guest returned a null invoke reply pointer');
260
+ }
261
+ assertMemoryRange(ex.memory.buffer, outPtr, outLen, 'invoke reply read');
218
262
  const out = new Uint8Array(ex.memory.buffer, outPtr, outLen).slice();
219
263
  ex.ck_free?.(outPtr, outLen);
220
264
  return out;