@opencode-ai/simulation 0.0.0-dev-17880 → 0.0.0-reserved

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.
@@ -1,485 +0,0 @@
1
- import { SdkPlugins } from "@opencode-ai/core/plugin/sdk";
2
- import { Tool } from "@opencode-ai/core/tool";
3
- import { Plugin } from "@opencode-ai/plugin/effect";
4
- import { createHash } from "node:crypto";
5
- import {
6
- Cause,
7
- Context,
8
- Deferred,
9
- Effect,
10
- Exit,
11
- Fiber,
12
- FiberSet,
13
- JsonSchema,
14
- Layer,
15
- PubSub,
16
- Queue,
17
- Ref,
18
- Schema,
19
- Scope,
20
- Semaphore,
21
- Stream
22
- } from "effect";
23
- import { SimulationControlServer } from "../control-server";
24
- import { SimulationProtocol } from "../protocol";
25
-
26
- export class ProviderDisconnectedError extends Schema.TaggedError()("SimulatedProvider.ProviderDisconnectedError", { message: Schema.String }) {
27
- }
28
-
29
- export class Service extends Context.Service()("@opencode/simulation/SimulatedProvider") {
30
- }
31
-
32
- class InvocationNotFoundError extends Schema.TaggedError()("SimulatedProvider.InvocationNotFoundError", { id: Schema.String, message: Schema.String }) {
33
- }
34
-
35
- class ControllerDisconnectedError extends Schema.TaggedError()("SimulatedProvider.ControllerDisconnectedError", { message: Schema.String }) {
36
- }
37
-
38
- class ToolInvocationNotFoundError extends Schema.TaggedError()("SimulatedProvider.ToolInvocationNotFoundError", { id: Schema.String, message: Schema.String }) {
39
- }
40
-
41
- class ToolControllerError extends Schema.TaggedError()("SimulatedProvider.ToolControllerError", {
42
- message: Schema.String
43
- }) {
44
- }
45
- export const layerDrive = (options) => Layer.effect(Service, Effect.gen(function* () {
46
- const state = yield* Ref.make({ counter: 0, pending: new Map }), opened = yield* PubSub.unbounded(), lock = yield* Semaphore.make(1), close = (invocation) => Effect.gen(function* () {
47
- yield* Queue.shutdown(invocation.responses);
48
- yield* lock.withPermit(Ref.update(state, (current) => current.pending.get(invocation.id) === invocation ? remove(current, invocation.id) : current));
49
- });
50
- yield* Effect.addFinalizer(() => Effect.gen(function* () {
51
- const current = yield* Ref.get(state);
52
- yield* Effect.forEach(current.pending.values(), (invocation) => Queue.shutdown(invocation.responses), {
53
- discard: !0
54
- });
55
- yield* PubSub.shutdown(opened);
56
- }));
57
- const open = (request) => lock.withPermit(Effect.gen(function* () {
58
- const current = yield* Ref.get(state), id = `inv_${current.counter + 1}`, responses = yield* Queue.bounded(256), invocation = { id, ...request, responses };
59
- yield* Ref.set(state, {
60
- counter: current.counter + 1,
61
- pending: new Map(current.pending).set(id, invocation)
62
- });
63
- yield* PubSub.publish(opened, { id, ...request });
64
- return invocation;
65
- })), requireInvocation = (id) => Effect.gen(function* () {
66
- const invocation = (yield* Ref.get(state)).pending.get(id);
67
- if (invocation)
68
- return invocation;
69
- return yield* Effect.fail(new InvocationNotFoundError({
70
- id,
71
- message: `Simulated provider invocation not found or already finished: ${id}`
72
- }));
73
- }), remove = (current, id) => {
74
- const pending = new Map(current.pending);
75
- pending.delete(id);
76
- return { ...current, pending };
77
- }, driver = {
78
- requests: Stream.unwrap(lock.withPermit(Effect.gen(function* () {
79
- const subscription = yield* PubSub.subscribe(opened), current = yield* Ref.get(state), pending = Array.from(current.pending.values(), ({ id, url, body }) => ({ id, url, body }));
80
- return Stream.concat(Stream.fromIterable(pending), Stream.fromEffectRepeat(PubSub.take(subscription)));
81
- }))),
82
- push: (id, items) => Effect.gen(function* () {
83
- const invocation = yield* lock.withPermit(requireInvocation(id));
84
- yield* Queue.offerAll(invocation.responses, items);
85
- }),
86
- finish: (id, reason) => Effect.gen(function* () {
87
- const invocation = yield* lock.withPermit(Effect.gen(function* () {
88
- const invocation = yield* requireInvocation(id), current = yield* Ref.get(state);
89
- yield* Ref.set(state, remove(current, id));
90
- return invocation;
91
- }));
92
- yield* Queue.offer(invocation.responses, { type: "finish", reason });
93
- yield* Queue.end(invocation.responses);
94
- }),
95
- disconnect: (id) => Effect.gen(function* () {
96
- const invocation = yield* lock.withPermit(Effect.gen(function* () {
97
- const invocation = yield* requireInvocation(id), current = yield* Ref.get(state);
98
- yield* Ref.set(state, remove(current, id));
99
- return invocation;
100
- }));
101
- yield* Queue.fail(invocation.responses, new ProviderDisconnectedError({ message: "Simulated model provider disconnected" }));
102
- }),
103
- pending: () => lock.withPermit(Ref.get(state).pipe(Effect.map((current) => Array.from(current.pending.values(), ({ id, url, body }) => ({ id, url, body })))))
104
- }, fibers = yield* FiberSet.make(), activeController = yield* Ref.make(void 0), controllerLock = yield* Semaphore.make(1), tools = yield* makeToolDriver();
105
- yield* Effect.addFinalizer(() => tools.shutdown);
106
- yield* SimulationControlServer.start({
107
- endpoint: options.endpoint,
108
- label: "opencode drive backend websocket",
109
- data: () => ({}),
110
- decode: SimulationProtocol.Backend.decodeRequestEffect,
111
- handle: (socket, request) => handle(driver, tools, fibers, activeController, controllerLock, socket, request, options.version),
112
- close: (socket) => Effect.all([releaseController(activeController, controllerLock, socket), tools.release(socket)], {
113
- discard: !0
114
- })
115
- });
116
- yield* Effect.sync(() => process.stderr.write(`opencode drive backend websocket: ${options.endpoint}
117
- `));
118
- return Service.of({
119
- stream: (request) => Stream.unwrap(Effect.acquireRelease(open(request), close).pipe(Effect.map((invocation) => Stream.fromQueue(invocation.responses).pipe(Stream.takeUntil((event) => event.type === "finish")))))
120
- });
121
- }));
122
- function handle(driver, tools, fibers, activeController, controllerLock, socket, request, version) {
123
- switch (request.method) {
124
- case "simulation.handshake":
125
- return SimulationProtocol.Handshake.dispatch({
126
- role: "backend",
127
- server: { name: "opencode", version },
128
- capabilities: SimulationProtocol.Backend.Capabilities
129
- }, request.params);
130
- case "llm.attach":
131
- return controllerLock.withPermit(Effect.gen(function* () {
132
- if (socket.data.closed)
133
- return yield* Effect.fail(new ControllerDisconnectedError({ message: "Drive controller disconnected before attachment" }));
134
- const previous = yield* Ref.get(activeController);
135
- if (previous)
136
- yield* Fiber.interrupt(previous);
137
- const attachment = yield* FiberSet.run(fibers, driver.requests.pipe(Stream.runForEach((invocation) => socket.send(JSON.stringify({ jsonrpc: "2.0", method: "llm.request", params: invocation })))));
138
- if (socket.data.closed) {
139
- yield* Fiber.interrupt(attachment);
140
- return yield* Effect.fail(new ControllerDisconnectedError({ message: "Drive controller disconnected during attachment" }));
141
- }
142
- socket.data.attachment = attachment;
143
- yield* Ref.set(activeController, attachment);
144
- return { attached: !0 };
145
- }));
146
- case "llm.chunk":
147
- return driver.push(request.params.id, request.params.items).pipe(Effect.as({ ok: !0 }));
148
- case "llm.finish":
149
- return driver.finish(request.params.id, request.params.reason).pipe(Effect.as({ ok: !0 }));
150
- case "llm.disconnect":
151
- return driver.disconnect(request.params.id).pipe(Effect.as({ ok: !0 }));
152
- case "llm.pending":
153
- return driver.pending().pipe(Effect.map((invocations) => ({ invocations })));
154
- case "tool.attach":
155
- return tools.attach(socket, request.params.tools);
156
- case "tool.update":
157
- return tools.update(socket, request.params).pipe(Effect.as({ ok: !0 }));
158
- case "tool.finish":
159
- return tools.finish(socket, request.params).pipe(Effect.as({ ok: !0 }));
160
- case "tool.fail":
161
- return tools.fail(socket, request.params).pipe(Effect.as({ ok: !0 }));
162
- }
163
- }
164
- const makeToolDriver = Effect.fn("SimulatedProvider.makeToolDriver")(function* () {
165
- const completedRetention = 256, plugins = yield* SdkPlugins.Service, state = yield* Ref.make({
166
- counter: 0,
167
- generation: 0,
168
- appliedGeneration: 0,
169
- registrations: [],
170
- pending: new Map,
171
- completed: new Map,
172
- activeOverlays: new Set,
173
- reconciliation: new Map
174
- }), registrationUpdates = yield* PubSub.unbounded(), lock = yield* Semaphore.make(1), attachmentLock = yield* Semaphore.make(1), remove = (current, id) => {
175
- const pending = new Map(current.pending);
176
- pending.delete(id);
177
- return { ...current, pending };
178
- }, complete = (current, id, completion) => {
179
- const completed = new Map(current.completed);
180
- completed.set(id, completion);
181
- if (completed.size > completedRetention) {
182
- const oldest = completed.keys().next().value;
183
- if (oldest !== void 0)
184
- completed.delete(oldest);
185
- }
186
- return { ...remove(current, id), completed };
187
- }, notify = (socket, method, params) => socket.send(JSON.stringify({ jsonrpc: "2.0", method, params })), requireController = (socket) => Effect.gen(function* () {
188
- const current = yield* Ref.get(state);
189
- if (current.controller?.socket === socket)
190
- return current;
191
- return yield* Effect.fail(new ToolControllerError({ message: "Drive tool controller is not attached" }));
192
- }), requireInvocation = (socket, id) => Effect.gen(function* () {
193
- const current = yield* requireController(socket), invocation = current.pending.get(id);
194
- if (invocation)
195
- return { current, invocation };
196
- return yield* Effect.fail(new ToolInvocationNotFoundError({
197
- id,
198
- message: `Simulated tool invocation not found or already finished: ${id}`
199
- }));
200
- }), cancel = (id) => Effect.gen(function* () {
201
- const invocation = yield* lock.withPermit(Ref.get(state).pipe(Effect.map((current) => current.pending.get(id))));
202
- if (!invocation)
203
- return;
204
- yield* invocation.operations.withPermit(lock.withPermit(Effect.gen(function* () {
205
- const current = yield* Ref.get(state);
206
- if (current.pending.get(id) !== invocation)
207
- return;
208
- yield* Ref.set(state, remove(current, id));
209
- if (current.controller && !current.controller.socket.data.closed)
210
- yield* notify(current.controller.socket, "tool.cancel", { id, reason: "interrupted" });
211
- })));
212
- }), invoke = (registrationGeneration, name, input, context) => Effect.gen(function* () {
213
- const encoded = yield* Schema.decodeUnknownEffect(Schema.Json)(input).pipe(Effect.mapError((error) => new Tool.Error({ message: `Simulated tool input is not JSON: ${error.message}` }))), invocation = yield* Effect.uninterruptibleMask((restore) => attachmentLock.withPermit(lock.withPermit(Effect.gen(function* () {
214
- const current = yield* Ref.get(state);
215
- if (current.generation !== registrationGeneration)
216
- yield* Effect.fail(new Tool.Error({ message: `Simulated tool registration is no longer active: ${name}` }));
217
- const id = `tool_${current.counter + 1}`, completion = yield* Deferred.make(), notification = {
218
- id,
219
- name,
220
- input: encoded,
221
- context: {
222
- sessionID: context.sessionID,
223
- agent: context.agent,
224
- messageID: context.messageID,
225
- id: context.id
226
- }
227
- }, pending = {
228
- id,
229
- notification,
230
- progress: context.progress,
231
- completion,
232
- operations: Semaphore.makeUnsafe(1)
233
- };
234
- yield* Ref.set(state, {
235
- counter: current.counter + 1,
236
- generation: current.generation,
237
- appliedGeneration: current.appliedGeneration,
238
- ...current.controller === void 0 ? {} : { controller: current.controller },
239
- registrations: current.registrations,
240
- pending: new Map(current.pending).set(id, pending),
241
- completed: current.completed,
242
- activeOverlays: current.activeOverlays,
243
- reconciliation: current.reconciliation
244
- });
245
- if (current.controller && !current.controller.socket.data.closed)
246
- yield* notify(current.controller.socket, "tool.invocation", notification);
247
- return pending;
248
- }))).pipe(Effect.flatMap((pending) => restore(Deferred.await(pending.completion)).pipe(Effect.onInterrupt(() => cancel(pending.id)), Effect.ensuring(lock.withPermit(Ref.update(state, (current) => current.pending.get(pending.id) === pending ? remove(current, pending.id) : current)))))));
249
- if (invocation.type === "success")
250
- return {
251
- output: invocation.output.structured,
252
- ...invocation.output.content.length === 0 ? {} : {
253
- content: invocation.output.content.map((part) => part.type === "text" ? part : {
254
- type: "file",
255
- uri: `data:${part.mime};base64,${part.data}`,
256
- mime: part.mime,
257
- ...part.name === void 0 ? {} : { name: part.name }
258
- })
259
- }
260
- };
261
- return yield* new Tool.Error({ message: invocation.message });
262
- });
263
- yield* plugins.register(Plugin.define({
264
- id: "opencode.simulation.tools",
265
- effect: (ctx) => Effect.gen(function* () {
266
- const scope = yield* Scope.Scope, token = {}, registrationLock = Semaphore.makeUnsafe(1);
267
- let currentScope;
268
- const reconcile = (generation, nextRegistrations) => registrationLock.withPermit(Effect.gen(function* () {
269
- const nextScope = yield* Scope.fork(scope), applied = yield* Effect.exit(ctx.tool.transform((draft) => {
270
- for (const registration of nextRegistrations)
271
- draft.add({
272
- name: registration.name,
273
- options: registration.permission === void 0 ? registration.options : { ...registration.options, permission: registration.permission },
274
- description: registration.description,
275
- input: registration.inputSchema,
276
- output: registration.outputSchema ?? {},
277
- execute: (input, context) => invoke(generation, SimulationProtocol.Backend.exposedToolName(registration), input, context)
278
- });
279
- }).pipe(Scope.provide(nextScope)));
280
- if (Exit.isFailure(applied)) {
281
- yield* Scope.close(nextScope, applied);
282
- yield* Effect.failCause(applied.cause);
283
- }
284
- const previousScope = currentScope;
285
- currentScope = nextScope;
286
- if (previousScope)
287
- yield* Scope.close(previousScope, Exit.void);
288
- })), acknowledge = (generation, result) => lock.withPermit(Effect.gen(function* () {
289
- const reconciliation = (yield* Ref.get(state)).reconciliation.get(token);
290
- if (reconciliation?.generation !== generation)
291
- return;
292
- yield* Deferred.succeed(reconciliation.result, result);
293
- })), initialized = yield* lock.withPermit(Effect.gen(function* () {
294
- const subscription = yield* PubSub.subscribe(registrationUpdates), current = yield* Ref.get(state), activeOverlays = new Set(current.activeOverlays).add(token);
295
- yield* Ref.set(state, { ...current, activeOverlays });
296
- return { subscription, generation: current.generation, registrations: current.registrations };
297
- }));
298
- yield* Effect.addFinalizer(() => lock.withPermit(Effect.gen(function* () {
299
- const current = yield* Ref.get(state), activeOverlays = new Set(current.activeOverlays);
300
- activeOverlays.delete(token);
301
- const reconciliation = new Map(current.reconciliation), pending = reconciliation.get(token);
302
- reconciliation.delete(token);
303
- yield* Ref.set(state, { ...current, activeOverlays, reconciliation });
304
- if (pending)
305
- yield* Deferred.succeed(pending.result, Exit.void);
306
- })));
307
- yield* reconcile(initialized.generation, initialized.registrations);
308
- yield* Stream.fromEffectRepeat(PubSub.take(initialized.subscription)).pipe(Stream.runForEach((update) => Effect.gen(function* () {
309
- const result = yield* Effect.exit(reconcile(update.generation, update.registrations));
310
- yield* acknowledge(update.generation, result);
311
- })), Effect.forkScoped({ startImmediately: !0 }));
312
- })
313
- }));
314
- const reconcileRegistrations = (controller, registrations, targetGeneration) => Effect.gen(function* () {
315
- const reconciliation = yield* lock.withPermit(Effect.gen(function* () {
316
- const current = yield* Ref.get(state), generation = targetGeneration ?? current.generation + 1, reconciliation = new Map;
317
- for (const token of current.activeOverlays)
318
- reconciliation.set(token, {
319
- generation,
320
- result: Deferred.makeUnsafe()
321
- });
322
- yield* Ref.set(state, {
323
- counter: current.counter,
324
- generation,
325
- appliedGeneration: current.appliedGeneration,
326
- ...controller === void 0 ? {} : { controller },
327
- registrations,
328
- pending: current.pending,
329
- completed: current.completed,
330
- activeOverlays: current.activeOverlays,
331
- reconciliation
332
- });
333
- yield* PubSub.publish(registrationUpdates, { generation, registrations });
334
- return { generation, previous: current, reconciliation };
335
- })), failure = (yield* Effect.forEach(reconciliation.reconciliation.values(), (item) => Deferred.await(item.result))).find(Exit.isFailure);
336
- yield* lock.withPermit(Ref.update(state, (current) => current.generation === reconciliation.generation ? {
337
- ...current,
338
- ...failure === void 0 ? { appliedGeneration: reconciliation.generation } : {},
339
- reconciliation: new Map
340
- } : current));
341
- return { failure, previous: reconciliation.previous };
342
- }), attach = (socket, registrations) => attachmentLock.withPermit(Effect.gen(function* () {
343
- if (socket.data.closed)
344
- return yield* Effect.fail(new ToolControllerError({ message: "Drive tool controller disconnected before attachment" }));
345
- const current = yield* Ref.get(state);
346
- if (current.controller && current.controller.socket !== socket && !current.controller.socket.data.closed)
347
- return yield* Effect.fail(new ToolControllerError({ message: "Another Drive tool controller is already attached" }));
348
- const controller = { socket }, replay = current.controller?.socket !== socket, sameRegistrations = current.appliedGeneration === current.generation && fingerprintJson(current.registrations) === fingerprintJson(registrations);
349
- if (replay && current.pending.size > 0 && !sameRegistrations)
350
- return yield* Effect.fail(new ToolControllerError({
351
- message: "A reconnecting Drive tool controller must settle pending invocations before replacing tools"
352
- }));
353
- if (sameRegistrations) {
354
- yield* lock.withPermit(Effect.gen(function* () {
355
- const current = yield* Ref.get(state);
356
- yield* Ref.set(state, { ...current, controller });
357
- if (replay)
358
- yield* Effect.forEach(current.pending.values(), (invocation) => notify(socket, "tool.invocation", invocation.notification), { discard: !0 });
359
- }));
360
- return { attached: !0 };
361
- }
362
- const reconciled = yield* reconcileRegistrations(controller, registrations);
363
- if (reconciled.failure) {
364
- const rolledBack = yield* reconcileRegistrations(reconciled.previous.controller, reconciled.previous.registrations, reconciled.previous.generation);
365
- return yield* Effect.fail(new ToolControllerError({
366
- message: rolledBack.failure ? `Failed to apply and restore simulated tools: ${Cause.pretty(reconciled.failure.cause)}; ${Cause.pretty(rolledBack.failure.cause)}` : `Failed to apply simulated tools: ${Cause.pretty(reconciled.failure.cause)}`
367
- }));
368
- }
369
- if (replay)
370
- yield* lock.withPermit(Effect.gen(function* () {
371
- const current = yield* Ref.get(state);
372
- if (current.controller?.socket !== socket)
373
- return;
374
- yield* Effect.forEach(current.pending.values(), (invocation) => notify(socket, "tool.invocation", invocation.notification), { discard: !0 });
375
- }));
376
- return { attached: !0 };
377
- })), update = (socket, params) => Effect.gen(function* () {
378
- const { invocation } = yield* lock.withPermit(requireInvocation(socket, params.id));
379
- yield* invocation.operations.withPermit(Effect.gen(function* () {
380
- if ((yield* lock.withPermit(Ref.get(state))).pending.get(params.id) !== invocation)
381
- yield* Effect.fail(new ToolInvocationNotFoundError({
382
- id: params.id,
383
- message: `Simulated tool invocation not found or already finished: ${params.id}`
384
- }));
385
- const fingerprint = fingerprintJson(params.update), applied = invocation.update;
386
- if (applied?.sequence === params.sequence && applied.fingerprint === fingerprint)
387
- return;
388
- if (applied?.sequence === params.sequence)
389
- yield* Effect.fail(new ToolControllerError({
390
- message: `Simulated tool update sequence ${params.sequence} was reused with different progress`
391
- }));
392
- const expected = applied === void 0 ? 0 : applied.sequence + 1;
393
- if (params.sequence !== expected)
394
- yield* Effect.fail(new ToolControllerError({
395
- message: `Expected simulated tool update sequence ${expected}, received ${params.sequence}`
396
- }));
397
- yield* invocation.progress(params.update);
398
- invocation.update = { sequence: params.sequence, fingerprint };
399
- }));
400
- }), settle = (socket, id, completion) => Effect.gen(function* () {
401
- const current = yield* lock.withPermit(requireController(socket)), fingerprint = fingerprintJson(completion), invocation = current.pending.get(id);
402
- if (!invocation) {
403
- if (current.completed.get(id) === fingerprint)
404
- return;
405
- yield* Effect.fail(new ToolInvocationNotFoundError({
406
- id,
407
- message: `Simulated tool invocation not found or already finished: ${id}`
408
- }));
409
- return;
410
- }
411
- yield* invocation.operations.withPermit(Effect.uninterruptible(lock.withPermit(Effect.gen(function* () {
412
- const current = yield* Ref.get(state);
413
- if (current.pending.get(id) !== invocation) {
414
- if (current.completed.get(id) === fingerprint)
415
- return;
416
- yield* Effect.fail(new ToolInvocationNotFoundError({
417
- id,
418
- message: `Simulated tool invocation not found or already finished: ${id}`
419
- }));
420
- return;
421
- }
422
- yield* Ref.set(state, complete(current, id, fingerprint));
423
- yield* Deferred.succeed(invocation.completion, completion);
424
- }))));
425
- }), release = (socket) => attachmentLock.withPermit(lock.withPermit(Ref.update(state, (current) => current.controller?.socket !== socket ? current : {
426
- counter: current.counter,
427
- generation: current.generation,
428
- appliedGeneration: current.appliedGeneration,
429
- registrations: current.registrations,
430
- pending: current.pending,
431
- completed: current.completed,
432
- activeOverlays: current.activeOverlays,
433
- reconciliation: current.reconciliation
434
- }))), shutdown = attachmentLock.withPermit(Effect.gen(function* () {
435
- yield* reconcileRegistrations(void 0, []);
436
- const current = yield* lock.withPermit(Effect.gen(function* () {
437
- const current = yield* Ref.get(state);
438
- yield* Ref.set(state, {
439
- counter: current.counter,
440
- generation: current.generation,
441
- appliedGeneration: current.appliedGeneration,
442
- registrations: [],
443
- pending: new Map,
444
- completed: current.completed,
445
- activeOverlays: new Set,
446
- reconciliation: new Map
447
- });
448
- return current;
449
- }));
450
- yield* Effect.forEach(current.pending.values(), (invocation) => Deferred.succeed(invocation.completion, {
451
- type: "failure",
452
- message: "Simulated tool controller shut down"
453
- }), { discard: !0 });
454
- yield* PubSub.shutdown(registrationUpdates);
455
- }));
456
- return {
457
- attach,
458
- update,
459
- finish: (socket, params) => settle(socket, params.id, { type: "success", output: params.output }),
460
- fail: (socket, params) => settle(socket, params.id, { type: "failure", message: params.message }),
461
- release,
462
- shutdown
463
- };
464
- });
465
- function canonicalJson(value) {
466
- if (Array.isArray(value))
467
- return `[${value.map(canonicalJson).join(",")}]`;
468
- if (typeof value === "object" && value !== null)
469
- return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
470
- return JSON.stringify(value) ?? String(value);
471
- }
472
- function fingerprintJson(value) {
473
- return createHash("sha256").update(canonicalJson(value)).digest("base64url");
474
- }
475
- function releaseController(activeController, controllerLock, socket) {
476
- return controllerLock.withPermit(Effect.gen(function* () {
477
- const attachment = socket.data.attachment;
478
- if (!attachment)
479
- return;
480
- yield* Fiber.interrupt(attachment);
481
- yield* Ref.update(activeController, (active) => active === attachment ? void 0 : active);
482
- }));
483
- }
484
-
485
- export * as SimulatedProvider from "./simulated-provider";
@@ -1,27 +0,0 @@
1
- import { Effect, Fiber } from "effect";
2
- export interface Server {
3
- readonly url: string;
4
- }
5
- interface Request {
6
- readonly id?: string | number | null;
7
- }
8
- export interface SocketData {
9
- readonly drive?: true;
10
- attachment?: Fiber.Fiber<void>;
11
- closed?: true;
12
- }
13
- export interface Socket {
14
- readonly data: SocketData;
15
- readonly send: (message: string) => Effect.Effect<void>;
16
- }
17
- export declare function start<RequestType extends Request, Error, Services>(options: {
18
- readonly endpoint: string;
19
- readonly label: string;
20
- readonly data: () => SocketData;
21
- readonly decode: (input: string) => Effect.Effect<RequestType, Error>;
22
- readonly handle: (socket: Socket, request: RequestType) => Effect.Effect<unknown, unknown, Services>;
23
- readonly close?: (socket: Socket) => Effect.Effect<void, never, Services>;
24
- }): Effect.Effect<{
25
- url: string;
26
- }, unknown, Services | import("effect/Scope").Scope>;
27
- export * as SimulationControlServer from "./control-server";
@@ -1,99 +0,0 @@
1
- import { Effect, Fiber, FiberSet, Queue, Stream } from "effect";
2
- import { SimulationProtocol } from "./protocol";
3
- const maxOutboundBytes = 67108864;
4
- export function start(options) {
5
- return Effect.gen(function* () {
6
- const messages = yield* Queue.bounded(256);
7
- yield* Stream.fromQueue(messages).pipe(Stream.runForEach((message) => options.decode(message.input).pipe(Effect.flatMap((request) => options.handle(message.socket, request).pipe(Effect.matchEffect({
8
- onFailure: (error) => send(message.socket, SimulationProtocol.JsonRpc.failure(request.id, error)),
9
- onSuccess: (result) => send(message.socket, SimulationProtocol.JsonRpc.success(request.id, result))
10
- }))), Effect.catch((error) => send(message.socket, SimulationProtocol.JsonRpc.failure(void 0, error))), Effect.catchCause((cause) => Effect.logWarning(`${options.label}: request failed`, cause)))), Effect.forkScoped);
11
- const url = yield* Effect.try({ try: () => new URL(options.endpoint), catch: (cause) => cause }), websocket = yield* Effect.promise(() => import("ws")), runPromise = yield* FiberSet.makeRuntimePromise();
12
- yield* Effect.acquireRelease(Effect.tryPromise(() => new Promise((resolve, reject) => {
13
- const server = new websocket.WebSocketServer({ host: url.hostname, port: Number(url.port) }), sockets = new Map, report = (scope, cause) => void runPromise(Effect.logWarning(`${options.label}: ${scope} error`, cause)), onServerError = (cause) => report("server", cause), onStartupError = (cause) => {
14
- server.off("listening", onListening);
15
- server.on("error", onServerError);
16
- reject(cause);
17
- }, onListening = () => {
18
- server.off("error", onStartupError);
19
- server.on("error", onServerError);
20
- resolve({
21
- close: async () => {
22
- const accepted = Array.from(sockets.entries());
23
- accepted.forEach(([connection, record]) => {
24
- record.socket.data.closed = !0;
25
- connection.terminate();
26
- });
27
- await Promise.all([
28
- new Promise((resolveClose, rejectClose) => {
29
- server.close((cause) => cause ? rejectClose(cause) : resolveClose());
30
- queueMicrotask(() => {
31
- if (server.address() === null)
32
- resolveClose();
33
- });
34
- }),
35
- Promise.all(accepted.map(([, record]) => record.cleanup()))
36
- ]);
37
- server.off("error", onServerError);
38
- }
39
- });
40
- };
41
- server.once("listening", onListening);
42
- server.once("error", onStartupError);
43
- server.on("connection", (connection) => {
44
- let pendingBytes = 0, outbound = Promise.resolve(), cleanup;
45
- const socket = {
46
- data: options.data(),
47
- send: (message) => Effect.tryPromise({
48
- try: () => {
49
- const bytes = Buffer.byteLength(message);
50
- if (bytes > maxOutboundBytes || pendingBytes + bytes > maxOutboundBytes)
51
- return Promise.reject(Error(`Simulation outbound queue exceeds ${maxOutboundBytes} bytes`));
52
- pendingBytes += bytes;
53
- const current = outbound.then(() => new Promise((resolveSend, rejectSend) => {
54
- if (connection.readyState !== websocket.WebSocket.OPEN) {
55
- rejectSend(Error("Simulation control socket is not open"));
56
- return;
57
- }
58
- connection.send(message, (cause) => cause ? rejectSend(cause) : resolveSend());
59
- }));
60
- outbound = current.catch(() => {
61
- return;
62
- });
63
- return current.finally(() => {
64
- pendingBytes -= bytes;
65
- });
66
- },
67
- catch: (cause) => cause instanceof Error ? cause : Error(String(cause))
68
- }).pipe(Effect.tapError(() => Effect.sync(() => {
69
- connection.terminate();
70
- })), Effect.orDie)
71
- }, close = () => {
72
- socket.data.closed = !0;
73
- cleanup ??= runPromise(options.close?.(socket) ?? Effect.void).finally(() => sockets.delete(connection));
74
- return cleanup;
75
- };
76
- sockets.set(connection, { socket, cleanup: close });
77
- connection.on("close", () => void close());
78
- connection.on("error", (cause) => {
79
- report("socket", cause);
80
- connection.terminate();
81
- });
82
- connection.on("message", (message) => {
83
- const input = Array.isArray(message) ? Buffer.concat(message).toString() : message instanceof ArrayBuffer ? Buffer.from(message).toString() : message.toString();
84
- if (Queue.offerUnsafe(messages, { socket, input }))
85
- return;
86
- runPromise(socket.send(JSON.stringify(SimulationProtocol.JsonRpc.failure(void 0, Error("Simulation control queue is full")))).pipe(Effect.catchCause((cause) => Effect.logWarning(`${options.label}: queue rejection failed`, cause))));
87
- });
88
- });
89
- })), ({ close }) => Effect.promise(close));
90
- return { url: options.endpoint };
91
- });
92
- }
93
- function send(socket, response) {
94
- if (!response)
95
- return Effect.void;
96
- return socket.send(JSON.stringify(response));
97
- }
98
-
99
- export * as SimulationControlServer from "./control-server";