ignotum 0.0.13 → 0.0.15
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.
- package/README.md +6 -6
- package/dist/cli/bin.mjs +6111 -4448
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/cli/emscripten-module-D5GzfUNv.mjs +533 -0
- package/dist/cli/emscripten-module-D5GzfUNv.mjs.map +1 -0
- package/dist/cli/ffi-7IElU4Co.mjs +148 -0
- package/dist/cli/ffi-7IElU4Co.mjs.map +1 -0
- package/dist/cli/managed-release-WHYLZwaS.mjs +10902 -0
- package/dist/cli/managed-release-WHYLZwaS.mjs.map +1 -0
- package/dist/cli/managed-release.d.mts +1 -0
- package/dist/cli/managed-release.mjs +14 -0
- package/dist/cli/managed-release.mjs.map +1 -0
- package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs +1289 -0
- package/dist/cli/module-ES6BEMUI-D36I4NVn.mjs.map +1 -0
- package/dist/runtime/{api-Cv3hMbzo.d.ts → api-B4fwp5a2.d.ts} +4 -4
- package/dist/runtime/bootstrap-BISQSn_O.js +916 -0
- package/dist/runtime/bootstrap-BISQSn_O.js.map +1 -0
- package/dist/runtime/bootstrap-BTFScKAc.d.ts +545 -0
- package/dist/runtime/client.d.ts +50 -15
- package/dist/runtime/client.js +7 -1138
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js +269 -0
- package/dist/runtime/descriptor-CTY4mtwS-cS43EbwS.js.map +1 -0
- package/dist/runtime/{id-Bt9XWRGL.js → id--1m0NuUL.js} +4 -4
- package/dist/runtime/id--1m0NuUL.js.map +1 -0
- package/dist/runtime/{id-BzFHf3Wo-DGPCjgrf.d.ts → id-7an3nxTu-DZJSM5xh.d.ts} +2 -2
- package/dist/runtime/id-CBOt2kDo.d.ts +1 -0
- package/dist/runtime/identity-QjtJRxBD.d.ts +2 -0
- package/dist/runtime/image-Djtjy4Z8.js +19 -0
- package/dist/runtime/image-Djtjy4Z8.js.map +1 -0
- package/dist/runtime/index-D4seNHlX.d.ts +184 -0
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/api.js +30 -1
- package/dist/runtime/internal/api.js.map +1 -0
- package/dist/runtime/internal/bootstrap.d.ts +2 -0
- package/dist/runtime/internal/bootstrap.js +2 -0
- package/dist/runtime/internal/client.d.ts +2 -0
- package/dist/runtime/internal/client.js +75 -0
- package/dist/runtime/internal/client.js.map +1 -0
- package/dist/runtime/internal/host.d.ts +25 -8
- package/dist/runtime/internal/host.js +27 -6
- package/dist/runtime/internal/host.js.map +1 -1
- package/dist/runtime/internal/routes.d.ts +2 -0
- package/dist/runtime/internal/routes.js +2 -0
- package/dist/runtime/internal/server.d.ts +1 -1
- package/dist/runtime/internal/server.js +1 -1
- package/dist/runtime/internal/types.d.ts +2 -1
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/pagination-CBOt2kDo.d.ts +1 -0
- package/dist/runtime/pagination-D3qd6s9N.js +33 -0
- package/dist/runtime/pagination-D3qd6s9N.js.map +1 -0
- package/dist/runtime/result-BkziOG1L.d.ts +1 -0
- package/dist/runtime/router-BSBI1oN1.js +2377 -0
- package/dist/runtime/router-BSBI1oN1.js.map +1 -0
- package/dist/runtime/routes-Ce8TVP-T.js +690 -0
- package/dist/runtime/routes-Ce8TVP-T.js.map +1 -0
- package/dist/runtime/{schema-1Zs03-iS.js → schema-D1wOqiNH.js} +40 -12
- package/dist/runtime/schema-D1wOqiNH.js.map +1 -0
- package/dist/runtime/server.d.ts +5 -5
- package/dist/runtime/server.js +2 -2
- package/dist/runtime/{sync-Bs8J3fIr.d.ts → sync-D-GK2sv9.d.ts} +3 -2
- package/dist/runtime/{api-CAgKDij7.js → sync-DzUwA8W9.js} +22 -41
- package/dist/runtime/sync-DzUwA8W9.js.map +1 -0
- package/dist/runtime/types-DeCCyqLU-BIm7rU-0.d.ts +341 -0
- package/package.json +29 -4
- package/src/cli/agent-files.ts +14 -8
- package/src/cli/build/managed-client.ts +411 -0
- package/src/cli/build/managed-server.ts +294 -0
- package/src/cli/build/managed.ts +67 -0
- package/src/cli/build/output.ts +47 -0
- package/src/cli/build/server.ts +51 -220
- package/src/cli/client-config.ts +1 -140
- package/src/cli/client-styles.ts +1 -0
- package/src/cli/codegen.ts +9 -4
- package/src/cli/command.ts +28 -5
- package/src/cli/control-client.ts +84 -101
- package/src/cli/deploy.ts +133 -177
- package/src/cli/dev.ts +506 -107
- package/src/cli/image-assets.ts +499 -0
- package/src/cli/managed-client-boundaries.ts +103 -0
- package/src/cli/managed-dev-platform.ts +118 -0
- package/src/cli/managed-exports.ts +219 -0
- package/src/cli/managed-release-bin.ts +13 -0
- package/src/cli/managed-release.ts +260 -0
- package/src/cli/module-boundaries.ts +11 -1
- package/src/cli/new-app.ts +111 -87
- package/src/cli/route-codegen.ts +311 -0
- package/src/cli/route-static.ts +710 -0
- package/src/client/bootstrap.ts +110 -0
- package/src/client/errors.ts +13 -7
- package/src/client/files.ts +64 -0
- package/src/client/id.ts +9 -5
- package/src/client/image.ts +28 -0
- package/src/client/index.ts +25 -2
- package/src/client/managed-client.ts +729 -0
- package/src/client/managed-upgrade.ts +283 -0
- package/src/client/recovery-journal.ts +195 -0
- package/src/client/route-boundaries.ts +184 -0
- package/src/client/router-history.ts +152 -0
- package/src/client/router-store.ts +644 -0
- package/src/client/router.ts +324 -0
- package/src/client/routes.ts +28 -0
- package/src/client/sync.ts +66 -592
- package/src/dev-runtime/database.ts +77 -2
- package/src/dev-runtime/functions.ts +3 -3
- package/src/dev-runtime/id.ts +19 -4
- package/src/dev-runtime/managed-functions.ts +255 -0
- package/src/dev-runtime/managed-sync.ts +263 -0
- package/src/dev-runtime/managed-websocket.ts +71 -0
- package/src/dev-runtime/migrations.ts +20 -0
- package/src/dev-runtime/sync.ts +236 -236
- package/src/image-assets.d.ts +36 -0
- package/src/internal/client.ts +186 -0
- package/src/internal/host.ts +1 -1
- package/src/internal/http-paths.ts +0 -3
- package/src/internal/routes.ts +45 -0
- package/dist/runtime/api-CAgKDij7.js.map +0 -1
- package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js +0 -154
- package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js.map +0 -1
- package/dist/runtime/id-Bt9XWRGL.js.map +0 -1
- package/dist/runtime/id-Dz0apuB3.d.ts +0 -1
- package/dist/runtime/index-D54flWtH.d.ts +0 -402
- package/dist/runtime/pagination-DnKg3dkI-r5ZUxBBx.d.ts +0 -112
- package/dist/runtime/pagination-Dz0apuB3.d.ts +0 -1
- package/dist/runtime/result-DKAA4gpS.d.ts +0 -1
- package/dist/runtime/schema-1Zs03-iS.js.map +0 -1
- package/src/cli/build/client.ts +0 -119
- package/src/cli/build/public.ts +0 -186
- package/src/cli/client-entry.ts +0 -152
- package/src/cli/client-plugin.ts +0 -136
- package/src/client/app.ts +0 -15
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { canonicalInvocationInput } from "@ignotum/contracts/runtime/hosted";
|
|
2
|
+
import { ManagedClientMessage, ManagedServerMessage } from "@ignotum/contracts/managed-sync";
|
|
3
|
+
import {
|
|
4
|
+
sameExecutionSelection,
|
|
5
|
+
ExecutionSelection,
|
|
6
|
+
type RecoveryIdentity,
|
|
7
|
+
type RecoverySessionId,
|
|
8
|
+
} from "@ignotum/contracts/platform";
|
|
9
|
+
import { idViewKey, type SessionState } from "@ignotum/contracts/id";
|
|
10
|
+
import type { ViteDevServer } from "vite";
|
|
11
|
+
import { Clock, Deferred, Effect, PubSub, Schema, Semaphore } from "effect";
|
|
12
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
13
|
+
import * as Socket from "effect/unstable/socket/Socket";
|
|
14
|
+
import { LocalApplicationFiles } from "./files.js";
|
|
15
|
+
import { MutationReplay, QueryInvalidation, createLocalSession } from "./sync.js";
|
|
16
|
+
|
|
17
|
+
export interface ManagedSyncOptions {
|
|
18
|
+
readonly selection: () => ExecutionSelection;
|
|
19
|
+
readonly loadModule?: ViteDevServer["ssrLoadModule"];
|
|
20
|
+
readonly isServerDependency?: (file: string) => boolean;
|
|
21
|
+
readonly beforeReload?: (file: string, event: "add" | "change" | "unlink") => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Native managed protocol framing, selection fencing, and durable invocation ownership. */
|
|
25
|
+
export const runManagedSession = Effect.fn("ManagedLocalSync.runSession")(function* (
|
|
26
|
+
socket: Socket.Socket,
|
|
27
|
+
options: ManagedSyncOptions,
|
|
28
|
+
identity: SessionState,
|
|
29
|
+
refreshIdentity?: Effect.Effect<SessionState>,
|
|
30
|
+
) {
|
|
31
|
+
const sql = yield* SqlClient.SqlClient;
|
|
32
|
+
const files = yield* LocalApplicationFiles;
|
|
33
|
+
const preparations = new Set<import("@ignotum/contracts/runtime/hosted").InvocationId>();
|
|
34
|
+
const shutdown = yield* Deferred.make<void>();
|
|
35
|
+
const replays = yield* MutationReplay;
|
|
36
|
+
const invalidation = yield* QueryInvalidation;
|
|
37
|
+
const write = yield* socket.writer;
|
|
38
|
+
const writes = yield* Semaphore.make(1);
|
|
39
|
+
const messages = yield* Semaphore.make(1);
|
|
40
|
+
let selection: ExecutionSelection | undefined;
|
|
41
|
+
let session: Effect.Success<ReturnType<typeof createLocalSession>> | undefined;
|
|
42
|
+
let clientSessionId: RecoverySessionId | undefined;
|
|
43
|
+
let expired = false;
|
|
44
|
+
const send = (message: ManagedServerMessage) =>
|
|
45
|
+
writes.withPermits(1)(
|
|
46
|
+
Schema.encodeEffect(Schema.fromJsonString(ManagedServerMessage))(message).pipe(
|
|
47
|
+
Effect.orDie,
|
|
48
|
+
Effect.flatMap(write),
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
const error = (
|
|
52
|
+
code:
|
|
53
|
+
| "InvalidMessage"
|
|
54
|
+
| "SessionExpired"
|
|
55
|
+
| "InvocationIdConflict"
|
|
56
|
+
| "Unavailable"
|
|
57
|
+
| "InvalidArguments",
|
|
58
|
+
message: string,
|
|
59
|
+
id?: string,
|
|
60
|
+
) =>
|
|
61
|
+
send(
|
|
62
|
+
id === undefined ? { type: "Error", code, message } : { type: "Error", code, message, id },
|
|
63
|
+
);
|
|
64
|
+
const close = Effect.gen(function* () {
|
|
65
|
+
expired = true;
|
|
66
|
+
yield* write(new Socket.CloseEvent(1000, "Local managed session ended")).pipe(
|
|
67
|
+
Effect.ensuring(Deferred.succeed(shutdown, undefined)),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
const checkSelection = Effect.fn("ManagedLocalSync.checkSelection")(function* () {
|
|
71
|
+
if (expired) return false;
|
|
72
|
+
if (selection === undefined || sameExecutionSelection(selection, options.selection()))
|
|
73
|
+
return true;
|
|
74
|
+
expired = true;
|
|
75
|
+
yield* send({ type: "UpgradeRequired", selection: options.selection() });
|
|
76
|
+
yield* close;
|
|
77
|
+
return false;
|
|
78
|
+
});
|
|
79
|
+
const checkIdentity = Effect.fn("ManagedLocalSync.checkIdentity")(function* () {
|
|
80
|
+
if (expired) return false;
|
|
81
|
+
const current = refreshIdentity === undefined ? identity : yield* refreshIdentity;
|
|
82
|
+
if (
|
|
83
|
+
current.sessionEpoch === identity.sessionEpoch &&
|
|
84
|
+
idViewKey(current) === idViewKey(identity) &&
|
|
85
|
+
current.validUntil > (yield* Clock.currentTimeMillis)
|
|
86
|
+
)
|
|
87
|
+
return true;
|
|
88
|
+
expired = true;
|
|
89
|
+
yield* error("SessionExpired", "The local ID session changed or expired.");
|
|
90
|
+
yield* close;
|
|
91
|
+
return false;
|
|
92
|
+
});
|
|
93
|
+
const events = yield* invalidation.subscribe;
|
|
94
|
+
yield* PubSub.take(events).pipe(
|
|
95
|
+
Effect.andThen(checkSelection()),
|
|
96
|
+
Effect.flatMap((current) => (current ? checkIdentity() : Effect.succeed(false))),
|
|
97
|
+
Effect.forever,
|
|
98
|
+
Effect.forkScoped,
|
|
99
|
+
);
|
|
100
|
+
const ownerRows = (invocationId: string) =>
|
|
101
|
+
sql<{
|
|
102
|
+
input: string;
|
|
103
|
+
prepared: number;
|
|
104
|
+
clientSessionId: string;
|
|
105
|
+
sessionEpoch: string;
|
|
106
|
+
selection: string;
|
|
107
|
+
}>`SELECT input, prepared, clientSessionId, sessionEpoch, selection FROM managed_invocation_owners WHERE id = ${invocationId}`;
|
|
108
|
+
const owns = (
|
|
109
|
+
row: { clientSessionId: string; sessionEpoch: string; selection: string },
|
|
110
|
+
original: ExecutionSelection,
|
|
111
|
+
) =>
|
|
112
|
+
row.clientSessionId === clientSessionId &&
|
|
113
|
+
row.sessionEpoch === identity.sessionEpoch &&
|
|
114
|
+
sameExecutionSelection(
|
|
115
|
+
Schema.decodeSync(Schema.fromJsonString(ExecutionSelection))(row.selection),
|
|
116
|
+
original,
|
|
117
|
+
);
|
|
118
|
+
const outcome = Effect.fn("ManagedLocalSync.outcome")(function* (
|
|
119
|
+
id: string,
|
|
120
|
+
recovery: RecoveryIdentity,
|
|
121
|
+
) {
|
|
122
|
+
if (
|
|
123
|
+
selection === undefined ||
|
|
124
|
+
recovery.originalSelection.appId !== selection.appId ||
|
|
125
|
+
recovery.originalSelection.dataHistoryId !== selection.dataHistoryId
|
|
126
|
+
) {
|
|
127
|
+
yield* error(
|
|
128
|
+
"Unavailable",
|
|
129
|
+
"This recovery record belongs to another application data history.",
|
|
130
|
+
id,
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const rows = yield* ownerRows(recovery.invocationId).pipe(Effect.orDie);
|
|
135
|
+
if (rows[0] === undefined || !owns(rows[0], recovery.originalSelection)) {
|
|
136
|
+
yield* error("SessionExpired", "This session does not own the mutation recovery record.", id);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const result = yield* replays.lookup(recovery.invocationId).pipe(Effect.orDie);
|
|
140
|
+
yield* send({
|
|
141
|
+
type: "MutationOutcome",
|
|
142
|
+
id,
|
|
143
|
+
identity: recovery,
|
|
144
|
+
outcome: result === undefined ? { type: "Uncertain" } : { type: "Settled", result },
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
const deliver = Effect.fn("ManagedLocalSync.deliver")(function* (message: ManagedServerMessage) {
|
|
148
|
+
if (!(yield* checkSelection()) || !(yield* checkIdentity())) return;
|
|
149
|
+
yield* send(message);
|
|
150
|
+
});
|
|
151
|
+
yield* Effect.addFinalizer(() =>
|
|
152
|
+
Effect.forEach(preparations, (id) => files.releasePreparation(id)),
|
|
153
|
+
);
|
|
154
|
+
yield* socket
|
|
155
|
+
.runString((text) =>
|
|
156
|
+
messages.withPermits(1)(
|
|
157
|
+
Effect.gen(function* () {
|
|
158
|
+
const message = yield* Schema.decodeEffect(Schema.fromJsonString(ManagedClientMessage))(
|
|
159
|
+
text,
|
|
160
|
+
).pipe(
|
|
161
|
+
Effect.catch(() =>
|
|
162
|
+
error("InvalidMessage", "The frame is not valid managed sync JSON."),
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
if (message === undefined) return;
|
|
166
|
+
if (message.type === "Connect") {
|
|
167
|
+
if (selection !== undefined)
|
|
168
|
+
return yield* error("InvalidMessage", "Connect has already been accepted.");
|
|
169
|
+
const current = options.selection();
|
|
170
|
+
if (
|
|
171
|
+
message.protocolVersion !== 1 ||
|
|
172
|
+
!sameExecutionSelection(message.selection, current)
|
|
173
|
+
) {
|
|
174
|
+
yield* send({ type: "UpgradeRequired", selection: current });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (!(yield* checkIdentity())) return;
|
|
178
|
+
selection = current;
|
|
179
|
+
clientSessionId = message.clientSessionId;
|
|
180
|
+
session = yield* createLocalSession(current, identity, deliver);
|
|
181
|
+
yield* send({
|
|
182
|
+
type: "Handshake",
|
|
183
|
+
protocolVersion: 1,
|
|
184
|
+
selection,
|
|
185
|
+
session: identity,
|
|
186
|
+
serverTime: yield* Clock.currentTimeMillis,
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (selection === undefined || session === undefined)
|
|
191
|
+
return yield* error("InvalidMessage", "Connect must be accepted first.");
|
|
192
|
+
if (expired || !(yield* checkSelection())) return;
|
|
193
|
+
if (!(yield* checkIdentity())) return;
|
|
194
|
+
if (message.type === "RecoverMutation")
|
|
195
|
+
return yield* outcome(message.id, message.identity);
|
|
196
|
+
if (message.type === "Invoke" || message.type === "Prepare") {
|
|
197
|
+
const input = canonicalInvocationInput(
|
|
198
|
+
selection.versionId,
|
|
199
|
+
"Mutation",
|
|
200
|
+
message.function,
|
|
201
|
+
message.args,
|
|
202
|
+
identity.sessionEpoch,
|
|
203
|
+
);
|
|
204
|
+
const rows = yield* ownerRows(message.invocationId).pipe(Effect.orDie);
|
|
205
|
+
if (rows[0] !== undefined && (!owns(rows[0], selection) || rows[0].input !== input))
|
|
206
|
+
return yield* error(
|
|
207
|
+
"InvocationIdConflict",
|
|
208
|
+
"This invocation belongs to another input, selection or session.",
|
|
209
|
+
message.invocationId,
|
|
210
|
+
);
|
|
211
|
+
if (
|
|
212
|
+
rows[0] === undefined &&
|
|
213
|
+
(yield* replays.lookup(message.invocationId).pipe(Effect.orDie)) !== undefined
|
|
214
|
+
)
|
|
215
|
+
return yield* error(
|
|
216
|
+
"InvocationIdConflict",
|
|
217
|
+
"This invocation already exists outside this managed session.",
|
|
218
|
+
message.invocationId,
|
|
219
|
+
);
|
|
220
|
+
const encodedSelection = yield* Schema.encodeEffect(
|
|
221
|
+
Schema.fromJsonString(ExecutionSelection),
|
|
222
|
+
)(selection).pipe(Effect.orDie);
|
|
223
|
+
const inserted =
|
|
224
|
+
yield* sql`INSERT OR IGNORE INTO managed_invocation_owners (id, input, prepared, clientSessionId, sessionEpoch, selection) VALUES (${message.invocationId}, ${input}, ${message.type === "Prepare" ? 1 : 0}, ${clientSessionId!}, ${identity.sessionEpoch}, ${encodedSelection}) RETURNING id`.pipe(
|
|
225
|
+
Effect.orDie,
|
|
226
|
+
);
|
|
227
|
+
const admitted = yield* ownerRows(message.invocationId).pipe(Effect.orDie);
|
|
228
|
+
if (
|
|
229
|
+
admitted[0] === undefined ||
|
|
230
|
+
admitted[0].input !== input ||
|
|
231
|
+
!owns(admitted[0], selection)
|
|
232
|
+
)
|
|
233
|
+
return yield* error(
|
|
234
|
+
"InvocationIdConflict",
|
|
235
|
+
"This invocation belongs to another session.",
|
|
236
|
+
message.invocationId,
|
|
237
|
+
);
|
|
238
|
+
if (inserted.length === 0) {
|
|
239
|
+
if (admitted[0].prepared === 0)
|
|
240
|
+
return yield* outcome(message.invocationId, {
|
|
241
|
+
invocationId: message.invocationId,
|
|
242
|
+
originalSelection: selection,
|
|
243
|
+
});
|
|
244
|
+
if (message.type === "Invoke") {
|
|
245
|
+
const claimed =
|
|
246
|
+
yield* sql`UPDATE managed_invocation_owners SET prepared = 0 WHERE id = ${message.invocationId} AND prepared = 1 RETURNING id`.pipe(
|
|
247
|
+
Effect.orDie,
|
|
248
|
+
);
|
|
249
|
+
if (claimed.length === 0)
|
|
250
|
+
return yield* outcome(message.invocationId, {
|
|
251
|
+
invocationId: message.invocationId,
|
|
252
|
+
originalSelection: selection,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
preparations.add(message.invocationId);
|
|
257
|
+
}
|
|
258
|
+
yield* session.handle(message);
|
|
259
|
+
}),
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
.pipe(Effect.raceFirst(Deferred.await(shutdown)));
|
|
263
|
+
}, Effect.scoped);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @effect-diagnostics-next-line nodeBuiltinImport:off Vite supplies Node HTTP upgrade requests.
|
|
2
|
+
import type { IncomingMessage } from "node:http";
|
|
3
|
+
// @effect-diagnostics-next-line nodeBuiltinImport:off Vite supplies the raw Node upgrade socket.
|
|
4
|
+
import type { Duplex } from "node:stream";
|
|
5
|
+
import { NodeSocket } from "@effect/platform-node";
|
|
6
|
+
import { Cause, Context, Effect, FiberSet, Function, Layer } from "effect";
|
|
7
|
+
import * as Socket from "effect/unstable/socket/Socket";
|
|
8
|
+
|
|
9
|
+
/** Owns upgraded sessions without sending an HTTP response when a session ends. */
|
|
10
|
+
const makeManagedWebSocketUpgradeHandler = Effect.fn("ManagedWebSocket.makeUpgradeHandler")(
|
|
11
|
+
function* <E, R>(
|
|
12
|
+
server: NodeSocket.NodeWS.WebSocketServer,
|
|
13
|
+
handle: (socket: Socket.Socket, request: IncomingMessage) => Effect.Effect<void, E, R>,
|
|
14
|
+
) {
|
|
15
|
+
const run = yield* FiberSet.makeRuntime<R>();
|
|
16
|
+
return (request: IncomingMessage, raw: Duplex, head: Buffer): void => {
|
|
17
|
+
const session = Effect.gen(function* () {
|
|
18
|
+
const socket = yield* Socket.fromWebSocket(
|
|
19
|
+
Effect.acquireRelease(
|
|
20
|
+
Effect.callback<globalThis.WebSocket>((resume) => {
|
|
21
|
+
server.handleUpgrade(request, raw, head, (webSocket) => {
|
|
22
|
+
// SAFETY: Effect's NodeSocket layer uses this same NodeWS/DOM bridge. Socket.fromWebSocket consumes compatible events, send and close methods.
|
|
23
|
+
// eslint-disable-next-line anti-slop/no-chained-type-assertions
|
|
24
|
+
resume(Effect.succeed(webSocket as unknown as globalThis.WebSocket));
|
|
25
|
+
});
|
|
26
|
+
}),
|
|
27
|
+
(webSocket) => Effect.sync(() => webSocket.close()),
|
|
28
|
+
),
|
|
29
|
+
{ closeCodeIsError: Function.constFalse },
|
|
30
|
+
);
|
|
31
|
+
yield* handle(socket, request);
|
|
32
|
+
}).pipe(
|
|
33
|
+
Effect.scoped,
|
|
34
|
+
Effect.catchCause((cause) =>
|
|
35
|
+
Cause.hasInterruptsOnly(cause)
|
|
36
|
+
? Effect.void
|
|
37
|
+
: Effect.logError("A managed local WebSocket session failed.").pipe(
|
|
38
|
+
Effect.annotateLogs({ cause }),
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
const fiber = run(session);
|
|
43
|
+
const onClose = () => fiber.interruptUnsafe();
|
|
44
|
+
raw.on("error", Function.constVoid);
|
|
45
|
+
raw.once("close", onClose);
|
|
46
|
+
fiber.addObserver(() => {
|
|
47
|
+
raw.off("error", Function.constVoid);
|
|
48
|
+
raw.off("close", onClose);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export class ManagedWebSocket extends Context.Service<
|
|
55
|
+
ManagedWebSocket,
|
|
56
|
+
{
|
|
57
|
+
readonly upgrade: (request: IncomingMessage, socket: Duplex, head: Buffer) => void;
|
|
58
|
+
}
|
|
59
|
+
>()("ignotum/dev-runtime/managed-websocket/ManagedWebSocket") {
|
|
60
|
+
static layer<E, R>(
|
|
61
|
+
server: NodeSocket.NodeWS.WebSocketServer,
|
|
62
|
+
handle: (socket: Socket.Socket, request: IncomingMessage) => Effect.Effect<void, E, R>,
|
|
63
|
+
) {
|
|
64
|
+
return Layer.effect(
|
|
65
|
+
ManagedWebSocket,
|
|
66
|
+
makeManagedWebSocketUpgradeHandler(server, handle).pipe(
|
|
67
|
+
Effect.map((upgrade) => ManagedWebSocket.of({ upgrade })),
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -88,10 +88,30 @@ const addApplicationFiles = Effect.gen(function* () {
|
|
|
88
88
|
`;
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
const addSchemaState = Effect.gen(function* () {
|
|
92
|
+
const sql = yield* SqlClient.SqlClient;
|
|
93
|
+
yield* sql`CREATE TABLE schema_state (
|
|
94
|
+
singleton INTEGER PRIMARY KEY NOT NULL CHECK (singleton = 1),
|
|
95
|
+
snapshot TEXT NOT NULL CHECK (json_valid(snapshot))
|
|
96
|
+
) STRICT`;
|
|
97
|
+
});
|
|
98
|
+
|
|
91
99
|
export const developmentMigrationLoader = Migrator.fromRecord({
|
|
92
100
|
"0001_initial_development_schema": initialDevelopmentSchema,
|
|
93
101
|
"0002_application_indexes": addDevelopmentIndexes,
|
|
94
102
|
"0003_application_files": addApplicationFiles,
|
|
103
|
+
"0004_schema_state": addSchemaState,
|
|
104
|
+
"0005_managed_invocation_owners": Effect.gen(function* () {
|
|
105
|
+
const sql = yield* SqlClient.SqlClient;
|
|
106
|
+
yield* sql`CREATE TABLE managed_invocation_owners (
|
|
107
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
108
|
+
input TEXT NOT NULL,
|
|
109
|
+
prepared INTEGER NOT NULL DEFAULT 0 CHECK (prepared IN (0, 1)),
|
|
110
|
+
clientSessionId TEXT NOT NULL,
|
|
111
|
+
sessionEpoch TEXT NOT NULL,
|
|
112
|
+
selection TEXT NOT NULL CHECK (json_valid(selection))
|
|
113
|
+
) STRICT`;
|
|
114
|
+
}),
|
|
95
115
|
});
|
|
96
116
|
|
|
97
117
|
interface DevelopmentDatabaseService {
|