ignotum 0.0.0 → 0.0.2
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 +161 -0
- package/dist/cli/bin.d.mts +1 -0
- package/dist/cli/bin.mjs +1696 -0
- package/dist/cli/bin.mjs.map +1 -0
- package/dist/runtime/api-BXXIZc_Q.js +119 -0
- package/dist/runtime/api-BXXIZc_Q.js.map +1 -0
- package/dist/runtime/api-Cpx57mk3.d.ts +47 -0
- package/dist/runtime/client/jsx-dev-runtime.d.ts +2 -0
- package/dist/runtime/client/jsx-dev-runtime.js +2 -0
- package/dist/runtime/client/jsx-runtime.d.ts +2 -0
- package/dist/runtime/client/jsx-runtime.js +2 -0
- package/dist/runtime/client.d.ts +29 -0
- package/dist/runtime/client.js +364 -0
- package/dist/runtime/client.js.map +1 -0
- package/dist/runtime/index-BgWROoyk.d.ts +249 -0
- package/dist/runtime/internal/api.d.ts +2 -0
- package/dist/runtime/internal/api.js +2 -0
- package/dist/runtime/internal/server.d.ts +6 -0
- package/dist/runtime/internal/server.js +7 -0
- package/dist/runtime/internal/server.js.map +1 -0
- package/dist/runtime/internal/types.d.ts +2 -0
- package/dist/runtime/internal/types.js +2 -0
- package/dist/runtime/result-B2W-z2wG.js +136 -0
- package/dist/runtime/result-B2W-z2wG.js.map +1 -0
- package/dist/runtime/result-C1ZdsM6Y.d.ts +106 -0
- package/dist/runtime/schema-CNEVLF7D.js +116 -0
- package/dist/runtime/schema-CNEVLF7D.js.map +1 -0
- package/dist/runtime/server.d.ts +9 -0
- package/dist/runtime/server.js +9 -0
- package/dist/runtime/server.js.map +1 -0
- package/package.json +81 -2
- package/src/cli/agent-files.ts +35 -0
- package/src/cli/bin.ts +5 -0
- package/src/cli/client-plugin.ts +66 -0
- package/src/cli/codegen.ts +203 -0
- package/src/cli/command.ts +155 -0
- package/src/cli/dev.ts +206 -0
- package/src/cli/new-project.ts +377 -0
- package/src/cli/package-manager.ts +55 -0
- package/src/client/errors.ts +83 -0
- package/src/client/hooks.ts +141 -0
- package/src/client/index.ts +90 -0
- package/src/client/jsx-dev-runtime.ts +2 -0
- package/src/client/jsx-runtime.ts +2 -0
- package/src/client/query.ts +17 -0
- package/src/client/sync.ts +487 -0
- package/src/dev-runtime/database.ts +374 -0
- package/src/dev-runtime/dev-database.ts +199 -0
- package/src/dev-runtime/functions.ts +293 -0
- package/src/dev-runtime/id.ts +11 -0
- package/src/dev-runtime/sync.ts +473 -0
- package/src/internal/api.ts +141 -0
- package/src/internal/http-paths.ts +5 -0
- package/src/internal/server.ts +3 -0
- package/src/internal/types.ts +2 -0
- package/src/raw.d.ts +4 -0
- package/src/server/index.ts +8 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
// @effect-diagnostics-next-line nodeBuiltinImport:off Vite exposes its HTTP server through Node's adapter types.
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
|
|
4
|
+
import { NodeHttpServer, NodeServices, NodeSocket } from "@effect/platform-node";
|
|
5
|
+
import { SqliteClient } from "@effect/sql-sqlite-node";
|
|
6
|
+
import {
|
|
7
|
+
ClientMessageJson,
|
|
8
|
+
ServerMessageJson,
|
|
9
|
+
type ClientMessage,
|
|
10
|
+
type FunctionAddress,
|
|
11
|
+
type Operation,
|
|
12
|
+
type ProtocolErrorCode,
|
|
13
|
+
type ServerMessage,
|
|
14
|
+
type SubscriptionId,
|
|
15
|
+
type WireResult,
|
|
16
|
+
} from "@ignotum/contracts/runtime/sync";
|
|
17
|
+
import {
|
|
18
|
+
Effect,
|
|
19
|
+
Context,
|
|
20
|
+
FileSystem,
|
|
21
|
+
Function,
|
|
22
|
+
FiberSet,
|
|
23
|
+
HashMap,
|
|
24
|
+
Layer,
|
|
25
|
+
ManagedRuntime,
|
|
26
|
+
Option,
|
|
27
|
+
Path,
|
|
28
|
+
PartitionedSemaphore,
|
|
29
|
+
PubSub,
|
|
30
|
+
Ref,
|
|
31
|
+
Schema,
|
|
32
|
+
Semaphore,
|
|
33
|
+
} from "effect";
|
|
34
|
+
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
|
|
35
|
+
import type * as Socket from "effect/unstable/socket/Socket";
|
|
36
|
+
import type { Plugin, ViteDevServer } from "vite";
|
|
37
|
+
import { FunctionRuntime, type PreparedFunction } from "@ignotum/runtime/functions";
|
|
38
|
+
import { QueryInvalidation } from "@ignotum/runtime/sync";
|
|
39
|
+
|
|
40
|
+
import { generate } from "../cli/codegen.js";
|
|
41
|
+
import { isIgnotumPath, syncPath } from "../internal/http-paths.js";
|
|
42
|
+
import { LocalDatabase } from "./database.js";
|
|
43
|
+
import { idGeneratorLayer } from "./id.js";
|
|
44
|
+
import { FunctionExecutor, FunctionRegistry, functionRuntimeLayer } from "./functions.js";
|
|
45
|
+
|
|
46
|
+
interface QuerySubscription {
|
|
47
|
+
readonly args: Schema.Json;
|
|
48
|
+
readonly function: FunctionAddress;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { QueryInvalidation };
|
|
52
|
+
|
|
53
|
+
export const queryInvalidationLayer = Layer.effect(
|
|
54
|
+
QueryInvalidation,
|
|
55
|
+
Effect.gen(function* () {
|
|
56
|
+
const pubsub = yield* PubSub.unbounded<void>();
|
|
57
|
+
return QueryInvalidation.of({
|
|
58
|
+
publish: PubSub.publish(pubsub, undefined),
|
|
59
|
+
subscribe: PubSub.subscribe(pubsub),
|
|
60
|
+
});
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const operationForQuery = (subscriptionId: SubscriptionId): Operation => ({
|
|
65
|
+
type: "Query",
|
|
66
|
+
id: subscriptionId,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const protocolError = (
|
|
70
|
+
code: ProtocolErrorCode,
|
|
71
|
+
message: string,
|
|
72
|
+
operation?: Operation,
|
|
73
|
+
): ServerMessage => {
|
|
74
|
+
if (operation === undefined) {
|
|
75
|
+
return { type: "ProtocolError", code, message };
|
|
76
|
+
}
|
|
77
|
+
return { type: "ProtocolError", code, message, operation };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const runSession = Effect.fn("SyncServer.runSession")(function* (socket: Socket.Socket) {
|
|
81
|
+
const runtime = yield* FunctionRuntime;
|
|
82
|
+
const invalidation = yield* QueryInvalidation;
|
|
83
|
+
const subscriptions = yield* Ref.make(HashMap.empty<SubscriptionId, QuerySubscription>());
|
|
84
|
+
const invocationFibers = yield* FiberSet.make();
|
|
85
|
+
const querySemaphore = yield* PartitionedSemaphore.make<SubscriptionId>({ permits: 1 });
|
|
86
|
+
const messageSemaphore = yield* Semaphore.make(1);
|
|
87
|
+
const writeSemaphore = yield* Semaphore.make(1);
|
|
88
|
+
const write = yield* socket.writer;
|
|
89
|
+
|
|
90
|
+
const send = Effect.fn("SyncServer.send")(function* (message: ServerMessage) {
|
|
91
|
+
yield* writeSemaphore.withPermits(1)(
|
|
92
|
+
Schema.encodeEffect(ServerMessageJson)(message).pipe(Effect.flatMap(write)),
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const sendResolutionError = (
|
|
97
|
+
operation: Operation,
|
|
98
|
+
error:
|
|
99
|
+
| { readonly _tag: "UnknownFunction"; readonly message: string }
|
|
100
|
+
| { readonly _tag: "WrongFunctionKind"; readonly message: string }
|
|
101
|
+
| { readonly _tag: "InvalidArguments"; readonly message: string }
|
|
102
|
+
| { readonly _tag: "FunctionUnavailable"; readonly message: string },
|
|
103
|
+
deliver: (
|
|
104
|
+
message: ServerMessage,
|
|
105
|
+
) => Effect.Effect<void, Schema.SchemaError | Socket.SocketError> = send,
|
|
106
|
+
) => {
|
|
107
|
+
const code: ProtocolErrorCode = error._tag;
|
|
108
|
+
return deliver(protocolError(code, error.message, operation));
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const isActive = (subscriptionId: SubscriptionId, subscription: QuerySubscription) =>
|
|
112
|
+
Ref.get(subscriptions).pipe(
|
|
113
|
+
Effect.map(
|
|
114
|
+
(current) => Option.getOrUndefined(HashMap.get(current, subscriptionId)) === subscription,
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const sendIfActive = Effect.fn("SyncServer.sendIfActive")(function* (
|
|
119
|
+
subscriptionId: SubscriptionId,
|
|
120
|
+
subscription: QuerySubscription,
|
|
121
|
+
message: ServerMessage,
|
|
122
|
+
) {
|
|
123
|
+
if (yield* isActive(subscriptionId, subscription)) {
|
|
124
|
+
yield* send(message);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const executeQuery = Effect.fn("SyncServer.executeQuery")(function* (
|
|
129
|
+
subscriptionId: SubscriptionId,
|
|
130
|
+
subscription: QuerySubscription,
|
|
131
|
+
previouslyPrepared?: PreparedFunction,
|
|
132
|
+
) {
|
|
133
|
+
if (!(yield* isActive(subscriptionId, subscription))) return;
|
|
134
|
+
|
|
135
|
+
const operation = operationForQuery(subscriptionId);
|
|
136
|
+
const deliver = (message: ServerMessage) => sendIfActive(subscriptionId, subscription, message);
|
|
137
|
+
const prepared =
|
|
138
|
+
previouslyPrepared ??
|
|
139
|
+
(yield* runtime.prepare(subscription.function, "Query", subscription.args).pipe(
|
|
140
|
+
Effect.catchTags({
|
|
141
|
+
FunctionUnavailable: (error) => sendResolutionError(operation, error, deliver),
|
|
142
|
+
InvalidArguments: (error) => sendResolutionError(operation, error, deliver),
|
|
143
|
+
UnknownFunction: (error) => sendResolutionError(operation, error, deliver),
|
|
144
|
+
WrongFunctionKind: (error) => sendResolutionError(operation, error, deliver),
|
|
145
|
+
}),
|
|
146
|
+
));
|
|
147
|
+
if (prepared === undefined) return;
|
|
148
|
+
|
|
149
|
+
const result = yield* prepared.execute;
|
|
150
|
+
yield* deliver({
|
|
151
|
+
type: "QuerySnapshot",
|
|
152
|
+
id: subscriptionId,
|
|
153
|
+
result,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const scheduleQuery = (
|
|
158
|
+
subscriptionId: SubscriptionId,
|
|
159
|
+
subscription: QuerySubscription,
|
|
160
|
+
previouslyPrepared?: PreparedFunction,
|
|
161
|
+
) =>
|
|
162
|
+
querySemaphore
|
|
163
|
+
.withPermits(
|
|
164
|
+
subscriptionId,
|
|
165
|
+
1,
|
|
166
|
+
)(executeQuery(subscriptionId, subscription, previouslyPrepared))
|
|
167
|
+
.pipe(
|
|
168
|
+
Effect.catchCause((cause) =>
|
|
169
|
+
Effect.logError("A query refresh fiber failed.").pipe(
|
|
170
|
+
Effect.annotateLogs({ cause, function: subscription.function, subscriptionId }),
|
|
171
|
+
),
|
|
172
|
+
),
|
|
173
|
+
FiberSet.run(invocationFibers),
|
|
174
|
+
Effect.asVoid,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const refresh = Effect.fn("SyncServer.refresh")(function* () {
|
|
178
|
+
const current = yield* Ref.get(subscriptions);
|
|
179
|
+
yield* Effect.forEach(HashMap.toEntries(current), ([subscriptionId, subscription]) =>
|
|
180
|
+
scheduleQuery(subscriptionId, subscription),
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const invalidations = yield* invalidation.subscribe;
|
|
185
|
+
yield* PubSub.take(invalidations).pipe(
|
|
186
|
+
Effect.flatMap(refresh),
|
|
187
|
+
Effect.forever,
|
|
188
|
+
Effect.forkScoped,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const handleSubscribe = Effect.fn("SyncServer.handleSubscribe")(function* (
|
|
192
|
+
message: Extract<ClientMessage, { readonly type: "Subscribe" }>,
|
|
193
|
+
) {
|
|
194
|
+
const operation = operationForQuery(message.id);
|
|
195
|
+
const current = yield* Ref.get(subscriptions);
|
|
196
|
+
|
|
197
|
+
if (HashMap.has(current, message.id)) {
|
|
198
|
+
yield* send(
|
|
199
|
+
protocolError(
|
|
200
|
+
"DuplicateOperationId",
|
|
201
|
+
`Subscription ${message.id} already exists.`,
|
|
202
|
+
operation,
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const prepared = yield* runtime.prepare(message.function, "Query", message.args).pipe(
|
|
209
|
+
Effect.catchTags({
|
|
210
|
+
FunctionUnavailable: (error) => sendResolutionError(operation, error),
|
|
211
|
+
InvalidArguments: (error) => sendResolutionError(operation, error),
|
|
212
|
+
UnknownFunction: (error) => sendResolutionError(operation, error),
|
|
213
|
+
WrongFunctionKind: (error) => sendResolutionError(operation, error),
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
if (prepared === undefined) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const subscription = { args: message.args, function: message.function };
|
|
222
|
+
yield* Ref.update(subscriptions, HashMap.set(message.id, subscription));
|
|
223
|
+
yield* scheduleQuery(message.id, subscription, prepared);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const handleMutate = Effect.fn("SyncServer.handleMutate")(function* (
|
|
227
|
+
message: Extract<ClientMessage, { readonly type: "Mutate" }>,
|
|
228
|
+
) {
|
|
229
|
+
const operation: Operation = { type: "Mutate", id: message.id };
|
|
230
|
+
const prepared = yield* runtime.prepare(message.function, "Mutation", message.args).pipe(
|
|
231
|
+
Effect.catchTags({
|
|
232
|
+
FunctionUnavailable: (error) => sendResolutionError(operation, error),
|
|
233
|
+
InvalidArguments: (error) => sendResolutionError(operation, error),
|
|
234
|
+
UnknownFunction: (error) => sendResolutionError(operation, error),
|
|
235
|
+
WrongFunctionKind: (error) => sendResolutionError(operation, error),
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (prepared === undefined) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
yield* prepared.execute.pipe(
|
|
244
|
+
Effect.flatMap((result: WireResult) => {
|
|
245
|
+
const response = send({ type: "MutationResult", id: message.id, result });
|
|
246
|
+
return result.type === "Success"
|
|
247
|
+
? response.pipe(Effect.ensuring(invalidation.publish))
|
|
248
|
+
: response;
|
|
249
|
+
}),
|
|
250
|
+
Effect.catchCause((cause) =>
|
|
251
|
+
Effect.logError("A mutation invocation fiber failed.").pipe(
|
|
252
|
+
Effect.annotateLogs({ cause, function: message.function, requestId: message.id }),
|
|
253
|
+
),
|
|
254
|
+
),
|
|
255
|
+
FiberSet.run(invocationFibers),
|
|
256
|
+
Effect.asVoid,
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const handleMessage = Effect.fn("SyncServer.handleMessage")(function* (text: string) {
|
|
261
|
+
const message = yield* Schema.decodeEffect(ClientMessageJson)(text).pipe(
|
|
262
|
+
Effect.catch(() =>
|
|
263
|
+
send(protocolError("InvalidMessage", "The WebSocket frame is not valid Ignotum JSON.")),
|
|
264
|
+
),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
if (message === undefined) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
switch (message.type) {
|
|
272
|
+
case "Subscribe":
|
|
273
|
+
yield* handleSubscribe(message);
|
|
274
|
+
return;
|
|
275
|
+
case "Unsubscribe":
|
|
276
|
+
yield* Ref.update(subscriptions, HashMap.remove(message.id));
|
|
277
|
+
return;
|
|
278
|
+
case "Mutate":
|
|
279
|
+
yield* handleMutate(message);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
yield* socket.runString((text) => messageSemaphore.withPermits(1)(handleMessage(text)));
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
interface SyncHandlersService {
|
|
288
|
+
readonly http: (request: IncomingMessage, response: ServerResponse) => void;
|
|
289
|
+
readonly upgrade: NodeJS.EventEmitter extends infer _EventEmitter
|
|
290
|
+
? (request: IncomingMessage, socket: import("node:stream").Duplex, head: Buffer) => void
|
|
291
|
+
: never;
|
|
292
|
+
readonly reload: (file: string, event: "add" | "change" | "unlink") => Effect.Effect<void>;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
class SyncHandlers extends Context.Service<SyncHandlers, SyncHandlersService>()(
|
|
296
|
+
"ignotum/dev-runtime/sync/SyncHandlers",
|
|
297
|
+
) {}
|
|
298
|
+
|
|
299
|
+
const makeHandlersLayer = (
|
|
300
|
+
server: ViteDevServer,
|
|
301
|
+
projectDirectory: string,
|
|
302
|
+
databasePath: string,
|
|
303
|
+
) => {
|
|
304
|
+
const databaseLayer = LocalDatabase.layer.pipe(
|
|
305
|
+
Layer.provide(Layer.merge(idGeneratorLayer, SqliteClient.layer({ filename: databasePath }))),
|
|
306
|
+
);
|
|
307
|
+
const executorLayer = FunctionExecutor.layer.pipe(Layer.provide(databaseLayer));
|
|
308
|
+
const devFunctionRuntimeLayer = functionRuntimeLayer.pipe(
|
|
309
|
+
Layer.provide(
|
|
310
|
+
Layer.merge(
|
|
311
|
+
FunctionRegistry.layer(server, projectDirectory).pipe(Layer.provide(NodeServices.layer)),
|
|
312
|
+
executorLayer,
|
|
313
|
+
),
|
|
314
|
+
),
|
|
315
|
+
);
|
|
316
|
+
const dependencies = Layer.mergeAll(
|
|
317
|
+
devFunctionRuntimeLayer,
|
|
318
|
+
queryInvalidationLayer,
|
|
319
|
+
NodeServices.layer,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
return Layer.effect(
|
|
323
|
+
SyncHandlers,
|
|
324
|
+
Effect.gen(function* () {
|
|
325
|
+
const invalidation = yield* QueryInvalidation;
|
|
326
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
327
|
+
const path = yield* Path.Path;
|
|
328
|
+
const scope = yield* Effect.scope;
|
|
329
|
+
const reloadSemaphore = yield* Semaphore.make(1);
|
|
330
|
+
const webSocketServer = yield* Effect.acquireRelease(
|
|
331
|
+
Effect.sync(
|
|
332
|
+
() => new NodeSocket.NodeWS.WebSocketServer({ maxPayload: 1024 * 1024, noServer: true }),
|
|
333
|
+
),
|
|
334
|
+
(instance) =>
|
|
335
|
+
Effect.callback<void>((resume) => {
|
|
336
|
+
instance.close(() => resume(Effect.void));
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
const httpApp = Effect.gen(function* () {
|
|
340
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
341
|
+
const pathname = new URL(request.url, "http://ignotum.local").pathname;
|
|
342
|
+
return yield* HttpServerResponse.json(
|
|
343
|
+
pathname === syncPath
|
|
344
|
+
? { code: "UpgradeRequired", message: `Connect to ${syncPath} with WebSocket.` }
|
|
345
|
+
: { code: "NotFound", message: "Unknown Ignotum API route." },
|
|
346
|
+
{ status: pathname === syncPath ? 426 : 404 },
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
const socketApp = Effect.gen(function* () {
|
|
350
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
351
|
+
const socket = yield* request.upgrade;
|
|
352
|
+
yield* runSession(socket);
|
|
353
|
+
return HttpServerResponse.empty();
|
|
354
|
+
});
|
|
355
|
+
const http = yield* NodeHttpServer.makeHandler(httpApp, { scope });
|
|
356
|
+
const upgrade = yield* NodeHttpServer.makeUpgradeHandler(
|
|
357
|
+
Effect.succeed(webSocketServer),
|
|
358
|
+
socketApp,
|
|
359
|
+
{ scope },
|
|
360
|
+
);
|
|
361
|
+
const serverDirectory = path.join(projectDirectory, "server");
|
|
362
|
+
const reloadUnsafe = Effect.fn("SyncServer.reload")(function* (
|
|
363
|
+
file: string,
|
|
364
|
+
event: "add" | "change" | "unlink",
|
|
365
|
+
) {
|
|
366
|
+
const relative = path.relative(serverDirectory, file);
|
|
367
|
+
if (relative.startsWith("..") || path.isAbsolute(relative) || !file.endsWith(".ts")) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (event !== "change") {
|
|
372
|
+
yield* generate(projectDirectory).pipe(
|
|
373
|
+
Effect.provideService(FileSystem.FileSystem, fileSystem),
|
|
374
|
+
Effect.provideService(Path.Path, path),
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
yield* invalidation.publish;
|
|
378
|
+
});
|
|
379
|
+
const reload = (file: string, event: "add" | "change" | "unlink") =>
|
|
380
|
+
reloadSemaphore
|
|
381
|
+
.withPermits(1)(reloadUnsafe(file, event))
|
|
382
|
+
.pipe(
|
|
383
|
+
Effect.catchCause((cause) =>
|
|
384
|
+
Effect.logError("Could not reload Ignotum server functions.").pipe(
|
|
385
|
+
Effect.annotateLogs({ cause, event, file }),
|
|
386
|
+
),
|
|
387
|
+
),
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
return SyncHandlers.of({ http, reload, upgrade });
|
|
391
|
+
}),
|
|
392
|
+
).pipe(Layer.provide(dependencies));
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const isApiPath = (request: IncomingMessage): boolean => {
|
|
396
|
+
const pathname = new URL(request.url ?? "/", "http://ignotum.local").pathname;
|
|
397
|
+
return isIgnotumPath(pathname);
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const isAppOrigin = (host: string | undefined, origin: string | undefined): boolean => {
|
|
401
|
+
if (host === undefined || origin === undefined) return false;
|
|
402
|
+
|
|
403
|
+
return Option.match(Schema.decodeOption(Schema.URLFromString)(origin), {
|
|
404
|
+
onNone: () => false,
|
|
405
|
+
onSome: (url) => url.origin === origin && url.host === host,
|
|
406
|
+
});
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const rejectUpgrade = (socket: import("node:stream").Duplex): void => {
|
|
410
|
+
socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n", () =>
|
|
411
|
+
socket.destroy(),
|
|
412
|
+
);
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
export const syncPluginInternals = { isAppOrigin };
|
|
416
|
+
|
|
417
|
+
export const ignotumSyncPlugin: {
|
|
418
|
+
(databasePath: string): (projectDirectory: string) => Plugin;
|
|
419
|
+
(projectDirectory: string, databasePath: string): Plugin;
|
|
420
|
+
} = Function.dual(2, (projectDirectory: string, databasePath: string): Plugin => ({
|
|
421
|
+
name: "ignotum:sync",
|
|
422
|
+
configureServer: (server) => {
|
|
423
|
+
if (server.httpServer === null) {
|
|
424
|
+
throw new Error("Ignotum sync requires Vite's Node HTTP server.");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const runtime = ManagedRuntime.make(makeHandlersLayer(server, projectDirectory, databasePath));
|
|
428
|
+
const httpServer = server.httpServer;
|
|
429
|
+
return runtime.runPromise(SyncHandlers).then(
|
|
430
|
+
(handlers) => {
|
|
431
|
+
const onUpgrade = (
|
|
432
|
+
request: IncomingMessage,
|
|
433
|
+
socket: import("node:stream").Duplex,
|
|
434
|
+
head: Buffer,
|
|
435
|
+
) => {
|
|
436
|
+
if (new URL(request.url ?? "/", "http://ignotum.local").pathname !== syncPath) return;
|
|
437
|
+
if (!isAppOrigin(request.headers.host, request.headers.origin)) {
|
|
438
|
+
rejectUpgrade(socket);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
handlers.upgrade(request, socket, head);
|
|
442
|
+
};
|
|
443
|
+
const onAdd = (file: string) => runtime.runFork(handlers.reload(file, "add"));
|
|
444
|
+
const onChange = (file: string) => runtime.runFork(handlers.reload(file, "change"));
|
|
445
|
+
const onUnlink = (file: string) => runtime.runFork(handlers.reload(file, "unlink"));
|
|
446
|
+
const cleanup = () => {
|
|
447
|
+
httpServer.off("upgrade", onUpgrade);
|
|
448
|
+
server.watcher.off("add", onAdd);
|
|
449
|
+
server.watcher.off("change", onChange);
|
|
450
|
+
server.watcher.off("unlink", onUnlink);
|
|
451
|
+
void runtime.dispose();
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
httpServer.on("upgrade", onUpgrade);
|
|
455
|
+
httpServer.once("close", cleanup);
|
|
456
|
+
server.watcher.on("add", onAdd);
|
|
457
|
+
server.watcher.on("change", onChange);
|
|
458
|
+
server.watcher.on("unlink", onUnlink);
|
|
459
|
+
server.middlewares.use((request, response, next) => {
|
|
460
|
+
if (!isApiPath(request)) {
|
|
461
|
+
next();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
handlers.http(request, response);
|
|
465
|
+
});
|
|
466
|
+
},
|
|
467
|
+
(error) => {
|
|
468
|
+
void runtime.dispose();
|
|
469
|
+
throw error;
|
|
470
|
+
},
|
|
471
|
+
);
|
|
472
|
+
},
|
|
473
|
+
}));
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Predicate } from "effect";
|
|
2
|
+
import type { Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import type { ErrorValue, InternalServerError } from "@ignotum/contracts/runtime/result";
|
|
5
|
+
import { FunctionAddress } from "@ignotum/contracts/runtime/sync";
|
|
6
|
+
|
|
7
|
+
const FunctionReferenceTypeId: unique symbol = Symbol.for("ignotum/internal/api/FunctionReference");
|
|
8
|
+
declare const FunctionReferenceTypesTypeId: unique symbol;
|
|
9
|
+
|
|
10
|
+
type FunctionKind = "Mutation" | "Query";
|
|
11
|
+
|
|
12
|
+
export interface FunctionReference<
|
|
13
|
+
Kind extends FunctionKind,
|
|
14
|
+
Args,
|
|
15
|
+
Success,
|
|
16
|
+
Failure extends ErrorValue,
|
|
17
|
+
> {
|
|
18
|
+
readonly [FunctionReferenceTypeId]: FunctionAddress;
|
|
19
|
+
readonly [FunctionReferenceTypesTypeId]?: {
|
|
20
|
+
readonly kind: Kind;
|
|
21
|
+
readonly args: Args;
|
|
22
|
+
readonly value: Success;
|
|
23
|
+
readonly error: Failure;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ReferenceTypes<Reference> = Reference extends {
|
|
28
|
+
readonly [FunctionReferenceTypesTypeId]?: infer Types;
|
|
29
|
+
}
|
|
30
|
+
? Exclude<Types, undefined>
|
|
31
|
+
: never;
|
|
32
|
+
|
|
33
|
+
export declare namespace FunctionReference {
|
|
34
|
+
type Args<Reference> =
|
|
35
|
+
ReferenceTypes<Reference> extends { readonly args: infer Args } ? Args : never;
|
|
36
|
+
type Failure<Reference> =
|
|
37
|
+
ReferenceTypes<Reference> extends {
|
|
38
|
+
readonly error: infer Failure;
|
|
39
|
+
}
|
|
40
|
+
? Failure
|
|
41
|
+
: never;
|
|
42
|
+
type Kind<Reference> =
|
|
43
|
+
ReferenceTypes<Reference> extends { readonly kind: infer Kind } ? Kind : never;
|
|
44
|
+
type Success<Reference> =
|
|
45
|
+
ReferenceTypes<Reference> extends {
|
|
46
|
+
readonly value: infer Success;
|
|
47
|
+
}
|
|
48
|
+
? Success
|
|
49
|
+
: never;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type ReferenceOf<Definition> = Definition extends {
|
|
53
|
+
readonly _tag: infer Kind extends FunctionKind;
|
|
54
|
+
readonly handler: (
|
|
55
|
+
...arguments_: infer HandlerArguments
|
|
56
|
+
) => Generator<infer Yielded, infer Success, never>;
|
|
57
|
+
}
|
|
58
|
+
? HandlerArguments extends readonly [infer _Context, ...infer Rest]
|
|
59
|
+
? FunctionReference<
|
|
60
|
+
Kind,
|
|
61
|
+
Rest extends readonly [infer Args, ...ReadonlyArray<unknown>] ? Args : void,
|
|
62
|
+
Success,
|
|
63
|
+
| (Yielded extends Effect.Effect<unknown, infer Failure extends ErrorValue, never>
|
|
64
|
+
? Failure
|
|
65
|
+
: never)
|
|
66
|
+
| InternalServerError
|
|
67
|
+
>
|
|
68
|
+
: never
|
|
69
|
+
: never;
|
|
70
|
+
|
|
71
|
+
type ApiModule<Module> = {
|
|
72
|
+
readonly [FunctionName in keyof Module as FunctionName extends string
|
|
73
|
+
? ReferenceOf<Module[FunctionName]> extends never
|
|
74
|
+
? never
|
|
75
|
+
: FunctionName
|
|
76
|
+
: never]: ReferenceOf<Module[FunctionName]>;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type Api<Modules> = {
|
|
80
|
+
readonly [ModuleName in keyof Modules]: ApiModule<Modules[ModuleName]>;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const functionPathOf = <
|
|
84
|
+
Kind extends FunctionKind,
|
|
85
|
+
Args,
|
|
86
|
+
Success,
|
|
87
|
+
Failure extends ErrorValue,
|
|
88
|
+
>(
|
|
89
|
+
reference: FunctionReference<Kind, Args, Success, Failure>,
|
|
90
|
+
) => reference[FunctionReferenceTypeId];
|
|
91
|
+
|
|
92
|
+
const makeModuleReference = (moduleName: string) => {
|
|
93
|
+
const references = new Map<string, object>();
|
|
94
|
+
|
|
95
|
+
return new Proxy(
|
|
96
|
+
{},
|
|
97
|
+
{
|
|
98
|
+
get: (_target, functionName) => {
|
|
99
|
+
if (!Predicate.isString(functionName)) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const existing = references.get(functionName);
|
|
104
|
+
if (existing !== undefined) {
|
|
105
|
+
return existing;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const reference = {
|
|
109
|
+
[FunctionReferenceTypeId]: FunctionAddress.make(`api.${moduleName}.${functionName}`),
|
|
110
|
+
};
|
|
111
|
+
references.set(functionName, reference);
|
|
112
|
+
return reference;
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export function createApi<Modules>(): Api<Modules>;
|
|
119
|
+
export function createApi() {
|
|
120
|
+
const modules = new Map<string, object>();
|
|
121
|
+
|
|
122
|
+
return new Proxy(
|
|
123
|
+
{},
|
|
124
|
+
{
|
|
125
|
+
get: (_target, moduleName) => {
|
|
126
|
+
if (!Predicate.isString(moduleName)) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const existing = modules.get(moduleName);
|
|
131
|
+
if (existing !== undefined) {
|
|
132
|
+
return existing;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const moduleReference = makeModuleReference(moduleName);
|
|
136
|
+
modules.set(moduleName, moduleReference);
|
|
137
|
+
return moduleReference;
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
}
|
package/src/raw.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Result as contractResult } from "@ignotum/contracts/runtime/result";
|
|
2
|
+
import type { ErrorValue, Result as ContractResult } from "@ignotum/contracts/runtime/result";
|
|
3
|
+
import { defineSchema as defineContractSchema } from "@ignotum/contracts/schema";
|
|
4
|
+
|
|
5
|
+
export const Result: typeof contractResult = contractResult;
|
|
6
|
+
export type Result<Success, Failure extends ErrorValue> = ContractResult<Success, Failure>;
|
|
7
|
+
export const defineSchema: typeof defineContractSchema = defineContractSchema;
|
|
8
|
+
export type { DefinedSchema, SchemaAuthoring } from "@ignotum/contracts/schema";
|