ignotum 0.0.10 → 0.0.12
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 +8 -3
- package/dist/cli/bin.mjs +563 -104
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/runtime/{api-jCl8Hzry.d.ts → api-5XSrIeqW.d.ts} +4 -3
- package/dist/runtime/client.d.ts +6 -5
- package/dist/runtime/client.js +170 -75
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/{descriptor-C5VA9qRl-BhD-WQeO.js → descriptor-XzDX2JDw-j3O6YHgW.js} +26 -6
- package/dist/runtime/descriptor-XzDX2JDw-j3O6YHgW.js.map +1 -0
- package/dist/runtime/{id-Cs82tq9Q-Caqfx54f.d.ts → id-Cs82tq9Q-CK-maMgN.d.ts} +13 -13
- package/dist/runtime/{index-D6VLTbDB.d.ts → index-CrWg4Z0y.d.ts} +132 -76
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/host.d.ts +18 -17
- package/dist/runtime/internal/host.js +11 -6
- package/dist/runtime/internal/host.js.map +1 -1
- package/dist/runtime/internal/server.d.ts +1 -1
- package/dist/runtime/internal/server.js +1 -1
- package/dist/runtime/internal/types.d.ts +1 -1
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/pagination-CX5IPbEi.d.ts +1 -0
- package/dist/runtime/{pagination-Bt3l7QaC-D_zI5zsu.d.ts → pagination-D-R9NR61-CpIoHFoI.d.ts} +8 -5
- package/dist/runtime/result-DIjKM-p4.d.ts +1 -0
- package/dist/runtime/{schema-C00wobuy.js → schema-B9XxyRO8.js} +132 -16
- package/dist/runtime/schema-B9XxyRO8.js.map +1 -0
- package/dist/runtime/server.d.ts +7 -3
- package/dist/runtime/server.js +5 -3
- package/dist/runtime/server.js.map +1 -1
- package/dist/runtime/{sync-bMQq9tXx.d.ts → sync-avN7NkcU.d.ts} +2 -2
- package/package.json +1 -1
- package/src/cli/agent-files.ts +2 -0
- package/src/cli/auth-client.ts +1 -1
- package/src/cli/bin.ts +13 -3
- package/src/cli/build/server.ts +102 -13
- package/src/cli/codegen.ts +7 -4
- package/src/cli/control-client.ts +30 -8
- package/src/cli/deploy.ts +7 -2
- package/src/cli/environment.ts +133 -0
- package/src/client/files.ts +43 -27
- package/src/client/hooks.ts +26 -17
- package/src/client/page-observers.ts +77 -0
- package/src/client/sync.ts +71 -59
- package/src/dev-runtime/functions.ts +127 -4
- package/src/dev-runtime/sync.ts +38 -9
- package/src/server/index.ts +11 -1
- package/dist/runtime/descriptor-C5VA9qRl-BhD-WQeO.js.map +0 -1
- package/dist/runtime/pagination-DcIkTOFs.d.ts +0 -1
- package/dist/runtime/schema-C00wobuy.js.map +0 -1
- package/src/dev-runtime/id.ts +0 -3
package/src/client/hooks.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
import { encodeTransportObject } from "@ignotum/contracts/runtime/sync";
|
|
13
13
|
import { ClientInfrastructureError } from "./errors.js";
|
|
14
14
|
import { isQuerySkip, type QuerySkip } from "./query.js";
|
|
15
|
+
// oxlint-disable-next-line anti-slop-effect/no-service-constructor-imports -- Each mounted pagination hook owns and releases its page observers.
|
|
16
|
+
import { makePageObservers } from "./page-observers.js";
|
|
15
17
|
import { SyncClient, syncClientInternals, syncRuntime } from "./sync.js";
|
|
16
18
|
|
|
17
19
|
// @effect-diagnostics-next-line missingPipeableSignature:off React hooks are not pipeable functions.
|
|
@@ -158,6 +160,18 @@ export function usePaginatedQuery<Args extends object, Item, Failure extends Err
|
|
|
158
160
|
readonly result: QueryResult<PaginatedQueryValue<Item>, Failure> | ClientInfrastructureError;
|
|
159
161
|
}>(() => ({ identity, result: pending() }));
|
|
160
162
|
const loadMore = useCallback(() => setRequestedPages((count) => count + 1), []);
|
|
163
|
+
const pages = useMemo(
|
|
164
|
+
() =>
|
|
165
|
+
makePageObservers(
|
|
166
|
+
() => setRefresh((version) => version + 1),
|
|
167
|
+
(release) => {
|
|
168
|
+
syncRuntime.runFork(release);
|
|
169
|
+
},
|
|
170
|
+
),
|
|
171
|
+
[identity, pageSize],
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
useEffect(() => () => pages.release(), [pages]);
|
|
161
175
|
|
|
162
176
|
useDebugValue({ args: input, function: functionPath, result: state.result });
|
|
163
177
|
|
|
@@ -170,12 +184,11 @@ export function usePaginatedQuery<Args extends object, Item, Failure extends Err
|
|
|
170
184
|
if (skipped) return;
|
|
171
185
|
|
|
172
186
|
let active = true;
|
|
173
|
-
const
|
|
187
|
+
const usedPages = new Set<string>();
|
|
174
188
|
|
|
175
189
|
void syncRuntime
|
|
176
190
|
.runPromise(
|
|
177
191
|
Effect.gen(function* () {
|
|
178
|
-
const client = yield* SyncClient;
|
|
179
192
|
const items: Item[] = [];
|
|
180
193
|
let cursor: PaginationOptions["cursor"] = null;
|
|
181
194
|
|
|
@@ -184,18 +197,9 @@ export function usePaginatedQuery<Args extends object, Item, Failure extends Err
|
|
|
184
197
|
...input,
|
|
185
198
|
pagination: { cursor, pageSize },
|
|
186
199
|
});
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const releaseSubscription = observer.subscribe(() => {
|
|
193
|
-
if (active) setRefresh((version) => version + 1);
|
|
194
|
-
});
|
|
195
|
-
releases.push(() => {
|
|
196
|
-
releaseSubscription();
|
|
197
|
-
syncRuntime.runFork(observer.release);
|
|
198
|
-
});
|
|
200
|
+
usedPages.add(syncClientInternals.queryIdentity(functionPath, pageArgs));
|
|
201
|
+
const observer = yield* pages.observe(functionPath, pageArgs);
|
|
202
|
+
if (!active || observer === undefined) return;
|
|
199
203
|
|
|
200
204
|
const snapshot = observer.getSnapshot();
|
|
201
205
|
if (Schema.is(ClientInfrastructureError)(snapshot)) {
|
|
@@ -240,7 +244,13 @@ export function usePaginatedQuery<Args extends object, Item, Failure extends Err
|
|
|
240
244
|
identity,
|
|
241
245
|
result: RuntimeResult.succeed({ items, loadMore, status: "CanLoadMore" }),
|
|
242
246
|
});
|
|
243
|
-
})
|
|
247
|
+
}).pipe(
|
|
248
|
+
Effect.ensuring(
|
|
249
|
+
Effect.sync(() => {
|
|
250
|
+
if (active) pages.retain(usedPages);
|
|
251
|
+
}),
|
|
252
|
+
),
|
|
253
|
+
),
|
|
244
254
|
)
|
|
245
255
|
.catch((error) => {
|
|
246
256
|
if (active && Schema.is(ClientInfrastructureError)(error)) {
|
|
@@ -250,9 +260,8 @@ export function usePaginatedQuery<Args extends object, Item, Failure extends Err
|
|
|
250
260
|
|
|
251
261
|
return () => {
|
|
252
262
|
active = false;
|
|
253
|
-
for (const release of releases) release();
|
|
254
263
|
};
|
|
255
|
-
}, [functionPath, identity, loadMore, pageSize, refresh, requestedPages, skipped]);
|
|
264
|
+
}, [functionPath, identity, loadMore, pageSize, pages, refresh, requestedPages, skipped]);
|
|
256
265
|
|
|
257
266
|
if (skipped || state.identity !== identity) return pending();
|
|
258
267
|
const result = state.result;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
import type { FunctionAddress } from "@ignotum/contracts/runtime/sync";
|
|
4
|
+
import { SyncClient, syncClientInternals } from "./sync.js";
|
|
5
|
+
import type { ClientInfrastructureError } from "./errors.js";
|
|
6
|
+
|
|
7
|
+
type QueryObserver = Effect.Success<ReturnType<SyncClient["Service"]["observe"]>>;
|
|
8
|
+
|
|
9
|
+
interface PageEntry {
|
|
10
|
+
readonly acquire: Effect.Effect<QueryObserver | undefined, ClientInfrastructureError, SyncClient>;
|
|
11
|
+
release: (() => void) | undefined;
|
|
12
|
+
released: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// @effect-diagnostics-next-line missingPipeableSignature:off This hook-owned lifecycle takes two callbacks, not a data argument.
|
|
16
|
+
export const makePageObservers = (
|
|
17
|
+
onChange: () => void,
|
|
18
|
+
releaseObserver: (release: Effect.Effect<void>) => void,
|
|
19
|
+
) => {
|
|
20
|
+
const pages = new Map<string, PageEntry>();
|
|
21
|
+
let disposed = false;
|
|
22
|
+
|
|
23
|
+
const releaseEntry = (entry: PageEntry) => {
|
|
24
|
+
entry.released = true;
|
|
25
|
+
entry.release?.();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const observe = Effect.fn("PageObservers.observe")(function* (
|
|
29
|
+
functionAddress: FunctionAddress,
|
|
30
|
+
args: Schema.Json,
|
|
31
|
+
) {
|
|
32
|
+
if (disposed) return;
|
|
33
|
+
const key = syncClientInternals.queryIdentity(functionAddress, args);
|
|
34
|
+
const existing = pages.get(key);
|
|
35
|
+
if (existing !== undefined) return yield* existing.acquire;
|
|
36
|
+
|
|
37
|
+
const entry: PageEntry = {
|
|
38
|
+
acquire: yield* Effect.cached(
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const client = yield* SyncClient;
|
|
41
|
+
const observer = yield* client.observe(functionAddress, args);
|
|
42
|
+
if (entry.released) {
|
|
43
|
+
releaseObserver(observer.release);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const unsubscribe = observer.subscribe(onChange);
|
|
47
|
+
entry.release = () => {
|
|
48
|
+
unsubscribe();
|
|
49
|
+
releaseObserver(observer.release);
|
|
50
|
+
};
|
|
51
|
+
return observer;
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
release: undefined,
|
|
55
|
+
released: false,
|
|
56
|
+
};
|
|
57
|
+
pages.set(key, entry);
|
|
58
|
+
return yield* entry.acquire;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
observe,
|
|
63
|
+
retain: (keys: ReadonlySet<string>) => {
|
|
64
|
+
for (const [key, entry] of pages) {
|
|
65
|
+
if (keys.has(key)) continue;
|
|
66
|
+
pages.delete(key);
|
|
67
|
+
releaseEntry(entry);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
release: () => {
|
|
71
|
+
if (disposed) return;
|
|
72
|
+
disposed = true;
|
|
73
|
+
for (const entry of pages.values()) releaseEntry(entry);
|
|
74
|
+
pages.clear();
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
};
|
package/src/client/sync.ts
CHANGED
|
@@ -75,11 +75,10 @@ type QueryLifecycleMessage = Extract<ClientMessage, { readonly type: "Subscribe"
|
|
|
75
75
|
interface QueryCache {
|
|
76
76
|
readonly entries: () => Iterable<QueryEntry>;
|
|
77
77
|
readonly getById: (id: SubscriptionId) => QueryEntry | undefined;
|
|
78
|
-
readonly setError: (id: SubscriptionId, error: ClientInfrastructureError) => void;
|
|
79
78
|
readonly setResult: (
|
|
80
79
|
id: SubscriptionId,
|
|
81
|
-
result: QueryResult<unknown, ErrorValue
|
|
82
|
-
revision
|
|
80
|
+
result: QueryResult<unknown, ErrorValue> | ClientInfrastructureError,
|
|
81
|
+
revision?: AppStateRevision,
|
|
83
82
|
) => void;
|
|
84
83
|
readonly observe: (
|
|
85
84
|
functionAddress: FunctionAddress,
|
|
@@ -102,7 +101,7 @@ const makeQueryCache = (
|
|
|
102
101
|
sendLifecycleMessage: (message: QueryLifecycleMessage) => Effect.Effect<void>,
|
|
103
102
|
) => {
|
|
104
103
|
const queries = new Map<string, QueryEntry>();
|
|
105
|
-
const
|
|
104
|
+
const queriesById = new Map<SubscriptionId, QueryEntry>();
|
|
106
105
|
|
|
107
106
|
const observe = Effect.fn("SyncClient.QueryCache.observe")(function* (
|
|
108
107
|
functionAddress: FunctionAddress,
|
|
@@ -123,7 +122,7 @@ const makeQueryCache = (
|
|
|
123
122
|
subscriptionId: yield* allocateId,
|
|
124
123
|
};
|
|
125
124
|
queries.set(key, created);
|
|
126
|
-
|
|
125
|
+
queriesById.set(created.subscriptionId, created);
|
|
127
126
|
yield* sendLifecycleMessage(subscribeMessage(created));
|
|
128
127
|
entry = created;
|
|
129
128
|
}
|
|
@@ -157,7 +156,7 @@ const makeQueryCache = (
|
|
|
157
156
|
}
|
|
158
157
|
|
|
159
158
|
queries.delete(key);
|
|
160
|
-
|
|
159
|
+
queriesById.delete(observed.subscriptionId);
|
|
161
160
|
yield* sendLifecycleMessage({
|
|
162
161
|
type: "Unsubscribe",
|
|
163
162
|
id: observed.subscriptionId,
|
|
@@ -168,28 +167,15 @@ const makeQueryCache = (
|
|
|
168
167
|
|
|
169
168
|
return {
|
|
170
169
|
entries: () => queries.values(),
|
|
171
|
-
getById: (id: SubscriptionId) =>
|
|
172
|
-
const key = queryKeysById.get(id);
|
|
173
|
-
return key === undefined ? undefined : queries.get(key);
|
|
174
|
-
},
|
|
170
|
+
getById: (id: SubscriptionId) => queriesById.get(id),
|
|
175
171
|
observe,
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
const entry = key === undefined ? undefined : queries.get(key);
|
|
179
|
-
if (entry === undefined) return;
|
|
180
|
-
entry.result = error;
|
|
181
|
-
for (const listener of entry.listeners) {
|
|
182
|
-
listener();
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
setResult: (id, result, revision) => {
|
|
186
|
-
const key = queryKeysById.get(id);
|
|
187
|
-
const entry = key === undefined ? undefined : queries.get(key);
|
|
172
|
+
setResult: (id, result, revision?: AppStateRevision) => {
|
|
173
|
+
const entry = queriesById.get(id);
|
|
188
174
|
if (entry === undefined) return;
|
|
189
|
-
if (
|
|
190
|
-
return;
|
|
175
|
+
if (revision !== undefined) {
|
|
176
|
+
if (entry.revision !== undefined && revision < entry.revision) return;
|
|
177
|
+
entry.revision = revision;
|
|
191
178
|
}
|
|
192
|
-
entry.revision = revision;
|
|
193
179
|
entry.result = result;
|
|
194
180
|
for (const listener of entry.listeners) {
|
|
195
181
|
listener();
|
|
@@ -202,15 +188,21 @@ type SnapshotFiles = NonNullable<Extract<ServerMessage, { readonly type: "Snapsh
|
|
|
202
188
|
|
|
203
189
|
const resultFromWire = Effect.fn("SyncClient.resultFromWire")(
|
|
204
190
|
(wire: WireResult, files: SnapshotFiles = []) =>
|
|
205
|
-
Effect.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
191
|
+
Effect.try({
|
|
192
|
+
try: () =>
|
|
193
|
+
wire.type === "Success"
|
|
194
|
+
? Result.succeed(
|
|
195
|
+
wire.value === undefined
|
|
196
|
+
? undefined
|
|
197
|
+
: applyFileGrants(decodeTransportValue(wire.value, wire.dates ?? []), files),
|
|
198
|
+
)
|
|
199
|
+
: failureFromWire(wire.error, wire.dates),
|
|
200
|
+
catch: (cause) =>
|
|
201
|
+
InvalidServerMessage.make({
|
|
202
|
+
cause,
|
|
203
|
+
message: "The server returned an invalid function result.",
|
|
204
|
+
}),
|
|
205
|
+
}),
|
|
214
206
|
);
|
|
215
207
|
|
|
216
208
|
export const shouldReloadForCloseCode = (code: number): boolean => code === DeploymentCloseCode;
|
|
@@ -344,10 +336,17 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
344
336
|
case "Snapshot": {
|
|
345
337
|
const entry = queryCache.getById(message.id);
|
|
346
338
|
if (entry === undefined) return;
|
|
339
|
+
if (entry.revision !== undefined && message.revision < entry.revision) return;
|
|
347
340
|
yield* resultFromWire(message.result, message.files ?? []).pipe(
|
|
348
341
|
Effect.tap((result) =>
|
|
349
342
|
Effect.sync(() => queryCache.setResult(message.id, result, message.revision)),
|
|
350
343
|
),
|
|
344
|
+
Effect.catchTags({
|
|
345
|
+
InvalidServerMessage: (error) => {
|
|
346
|
+
queryCache.setResult(message.id, error, message.revision);
|
|
347
|
+
return reportClientError(error);
|
|
348
|
+
},
|
|
349
|
+
}),
|
|
351
350
|
);
|
|
352
351
|
return;
|
|
353
352
|
}
|
|
@@ -361,9 +360,14 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
361
360
|
case "Result": {
|
|
362
361
|
const pendingInvocation = HashMap.getUnsafe(invocations, message.id);
|
|
363
362
|
if (pendingInvocation === undefined) return;
|
|
364
|
-
invocations = HashMap.remove(invocations, message.id);
|
|
365
363
|
yield* resultFromWire(message.result).pipe(
|
|
366
|
-
Effect.flatMap((result) =>
|
|
364
|
+
Effect.flatMap((result) => {
|
|
365
|
+
invocations = HashMap.remove(invocations, message.id);
|
|
366
|
+
return Deferred.succeed(pendingInvocation.deferred, result);
|
|
367
|
+
}),
|
|
368
|
+
Effect.catchTags({
|
|
369
|
+
InvalidServerMessage: (error) => rejectInvocation(message.id, error),
|
|
370
|
+
}),
|
|
367
371
|
);
|
|
368
372
|
return;
|
|
369
373
|
}
|
|
@@ -382,7 +386,7 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
382
386
|
if (message.operation?.type === "Invocation") {
|
|
383
387
|
yield* rejectInvocation(message.operation.id, error);
|
|
384
388
|
} else if (message.operation?.type === "Subscription") {
|
|
385
|
-
queryCache.
|
|
389
|
+
queryCache.setResult(message.operation.id, error);
|
|
386
390
|
yield* reportClientError(error);
|
|
387
391
|
} else {
|
|
388
392
|
yield* reportClientError(error);
|
|
@@ -573,6 +577,7 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
573
577
|
const uploads = yield* Deferred.await(prepared);
|
|
574
578
|
const expected = new Set(message.files);
|
|
575
579
|
const received = new Set<string>();
|
|
580
|
+
const transfers: Array<{ readonly file: File; readonly url: string }> = [];
|
|
576
581
|
for (const upload of uploads) {
|
|
577
582
|
if (!expected.has(upload.id) || received.has(upload.id)) {
|
|
578
583
|
return yield* InvalidServerMessage.make({
|
|
@@ -589,28 +594,7 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
589
594
|
});
|
|
590
595
|
}
|
|
591
596
|
if (upload.url === undefined) continue;
|
|
592
|
-
|
|
593
|
-
const response = yield* Effect.tryPromise({
|
|
594
|
-
try: () =>
|
|
595
|
-
fetch(uploadUrl, {
|
|
596
|
-
method: "PUT",
|
|
597
|
-
body: file,
|
|
598
|
-
headers: { "content-type": file.type },
|
|
599
|
-
}),
|
|
600
|
-
catch: (cause) =>
|
|
601
|
-
ConnectionUnavailable.make({
|
|
602
|
-
cause,
|
|
603
|
-
message: `File '${file.name}' could not be uploaded.`,
|
|
604
|
-
}),
|
|
605
|
-
});
|
|
606
|
-
if (!response.ok) {
|
|
607
|
-
const detail = yield* Effect.promise(() => response.text());
|
|
608
|
-
return yield* InvalidMutationArguments.make({
|
|
609
|
-
cause: new Error(detail),
|
|
610
|
-
function: functionAddress,
|
|
611
|
-
message: `File '${file.name}' was rejected by application storage.`,
|
|
612
|
-
});
|
|
613
|
-
}
|
|
597
|
+
transfers.push({ file, url: upload.url });
|
|
614
598
|
}
|
|
615
599
|
if (received.size !== expected.size) {
|
|
616
600
|
return yield* InvalidServerMessage.make({
|
|
@@ -618,6 +602,34 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
618
602
|
message: "The server returned incomplete application file preparation data.",
|
|
619
603
|
});
|
|
620
604
|
}
|
|
605
|
+
yield* Effect.forEach(
|
|
606
|
+
transfers,
|
|
607
|
+
Effect.fn("SyncClient.uploadFile")(function* ({ file, url }) {
|
|
608
|
+
const response = yield* Effect.tryPromise({
|
|
609
|
+
try: (signal) =>
|
|
610
|
+
fetch(url, {
|
|
611
|
+
method: "PUT",
|
|
612
|
+
body: file,
|
|
613
|
+
headers: { "content-type": file.type },
|
|
614
|
+
signal,
|
|
615
|
+
}),
|
|
616
|
+
catch: (cause) =>
|
|
617
|
+
ConnectionUnavailable.make({
|
|
618
|
+
cause,
|
|
619
|
+
message: `File '${file.name}' could not be uploaded.`,
|
|
620
|
+
}),
|
|
621
|
+
});
|
|
622
|
+
if (!response.ok) {
|
|
623
|
+
const detail = yield* Effect.promise(() => response.text());
|
|
624
|
+
return yield* InvalidMutationArguments.make({
|
|
625
|
+
cause: new Error(detail),
|
|
626
|
+
function: functionAddress,
|
|
627
|
+
message: `File '${file.name}' was rejected by application storage.`,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}),
|
|
631
|
+
{ concurrency: 3, discard: true },
|
|
632
|
+
);
|
|
621
633
|
const pendingInvocation = HashMap.getUnsafe(invocations, invocationId);
|
|
622
634
|
if (pendingInvocation === undefined) return yield* Deferred.await(deferred);
|
|
623
635
|
invocations = HashMap.set(invocations, invocationId, {
|
|
@@ -6,12 +6,21 @@ import {
|
|
|
6
6
|
type FunctionKind,
|
|
7
7
|
type FunctionResolutionError,
|
|
8
8
|
} from "@ignotum/contracts/runtime/functions";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
getFunctionEnvironment,
|
|
11
|
+
type RuntimeSchemaDefinition,
|
|
12
|
+
} from "@ignotum/contracts/runtime/schema";
|
|
10
13
|
import { ErrorValueSchema, type ErrorValue } from "@ignotum/contracts/runtime/result";
|
|
11
14
|
import { RequestId } from "@ignotum/contracts/runtime/id";
|
|
12
15
|
import { IdGenerator } from "@ignotum/shared/id";
|
|
13
16
|
import { encodedFileOccurrencesOf } from "@ignotum/contracts/schema/file";
|
|
14
17
|
import type { RuntimeInvocationResult } from "@ignotum/contracts/runtime/hosted";
|
|
18
|
+
import {
|
|
19
|
+
emptyEnv,
|
|
20
|
+
isDefinedEnv,
|
|
21
|
+
type DefinedEnv,
|
|
22
|
+
type EnvironmentType,
|
|
23
|
+
} from "@ignotum/contracts/schema";
|
|
15
24
|
import {
|
|
16
25
|
FunctionAddress,
|
|
17
26
|
apiFunctionParts,
|
|
@@ -21,7 +30,19 @@ import {
|
|
|
21
30
|
type WireResult,
|
|
22
31
|
} from "@ignotum/contracts/runtime/sync";
|
|
23
32
|
import { FunctionRuntime } from "@ignotum/runtime/functions";
|
|
24
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
Cause,
|
|
35
|
+
Context,
|
|
36
|
+
Effect,
|
|
37
|
+
FileSystem,
|
|
38
|
+
Layer,
|
|
39
|
+
Path,
|
|
40
|
+
Predicate,
|
|
41
|
+
Ref,
|
|
42
|
+
Result,
|
|
43
|
+
Schema,
|
|
44
|
+
Semaphore,
|
|
45
|
+
} from "effect";
|
|
25
46
|
import type { ViteDevServer } from "vite";
|
|
26
47
|
import { normalizePath } from "vite";
|
|
27
48
|
|
|
@@ -31,6 +52,11 @@ import {
|
|
|
31
52
|
type FunctionArguments,
|
|
32
53
|
type RuntimeFunctionDefinition as InternalRuntimeFunctionDefinition,
|
|
33
54
|
} from "../internal/function-definition.js";
|
|
55
|
+
import {
|
|
56
|
+
EnvironmentConfigurationInvalid,
|
|
57
|
+
loadEnvironment,
|
|
58
|
+
type LoadedEnvironment,
|
|
59
|
+
} from "../cli/environment.js";
|
|
34
60
|
|
|
35
61
|
export { FunctionUnavailable, InvalidArguments, UnknownFunction, WrongFunctionKind };
|
|
36
62
|
export type { FunctionArguments, FunctionKind };
|
|
@@ -38,10 +64,78 @@ export type { FunctionArguments, FunctionKind };
|
|
|
38
64
|
export type RuntimeHandlerContext = LocalMutationContext | LocalQueryContext;
|
|
39
65
|
export type RuntimeFunctionDefinition = InternalRuntimeFunctionDefinition<RuntimeHandlerContext>;
|
|
40
66
|
|
|
67
|
+
const loadDevelopmentEnvironment = Effect.fn("DevelopmentEnvironment.load")(function* (
|
|
68
|
+
server: ViteDevServer,
|
|
69
|
+
appDirectory: string,
|
|
70
|
+
) {
|
|
71
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
72
|
+
const path = yield* Path.Path;
|
|
73
|
+
const definitionPath = path.join(appDirectory, "server", "env.ts");
|
|
74
|
+
const hasDefinition = yield* fileSystem.exists(definitionPath);
|
|
75
|
+
if (!hasDefinition) return yield* loadEnvironment(appDirectory, emptyEnv, false);
|
|
76
|
+
const loaded = yield* Effect.tryPromise({
|
|
77
|
+
try: () => server.ssrLoadModule(normalizePath(definitionPath)),
|
|
78
|
+
catch: () =>
|
|
79
|
+
EnvironmentConfigurationInvalid.make({
|
|
80
|
+
message: "Ignotum could not load server/env.ts.",
|
|
81
|
+
path: definitionPath,
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
if (!Predicate.isObject(loaded.default) || !isDefinedEnv(loaded.default)) {
|
|
85
|
+
return yield* EnvironmentConfigurationInvalid.make({
|
|
86
|
+
message: "The default export from server/env.ts is not an Ignotum environment.",
|
|
87
|
+
path: definitionPath,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return yield* loadEnvironment(appDirectory, loaded.default, true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export class DevelopmentEnvironment extends Context.Service<
|
|
94
|
+
DevelopmentEnvironment,
|
|
95
|
+
{
|
|
96
|
+
readonly reload: Effect.Effect<void, unknown>;
|
|
97
|
+
readonly snapshot: Effect.Effect<LoadedEnvironment, unknown>;
|
|
98
|
+
}
|
|
99
|
+
>()("ignotum/dev-runtime/functions/DevelopmentEnvironment") {
|
|
100
|
+
static layer(server: ViteDevServer, appDirectory: string) {
|
|
101
|
+
return Layer.effect(
|
|
102
|
+
DevelopmentEnvironment,
|
|
103
|
+
Effect.gen(function* () {
|
|
104
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
105
|
+
const path = yield* Path.Path;
|
|
106
|
+
const load = loadDevelopmentEnvironment(server, appDirectory).pipe(
|
|
107
|
+
Effect.provideService(FileSystem.FileSystem, fileSystem),
|
|
108
|
+
Effect.provideService(Path.Path, path),
|
|
109
|
+
);
|
|
110
|
+
const initial = yield* load;
|
|
111
|
+
const current = yield* Ref.make<Result.Result<LoadedEnvironment, unknown>>(
|
|
112
|
+
Result.succeed(initial),
|
|
113
|
+
);
|
|
114
|
+
const fromResult = Result.match({
|
|
115
|
+
onFailure: Effect.fail,
|
|
116
|
+
onSuccess: Effect.succeed,
|
|
117
|
+
});
|
|
118
|
+
const reload = load.pipe(
|
|
119
|
+
Effect.result,
|
|
120
|
+
Effect.flatMap((result) =>
|
|
121
|
+
Ref.set(current, result).pipe(Effect.andThen(fromResult(result))),
|
|
122
|
+
),
|
|
123
|
+
Effect.asVoid,
|
|
124
|
+
);
|
|
125
|
+
return DevelopmentEnvironment.of({
|
|
126
|
+
reload,
|
|
127
|
+
snapshot: Ref.get(current).pipe(Effect.flatMap(fromResult)),
|
|
128
|
+
});
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
41
134
|
export interface ResolvedFunction {
|
|
42
135
|
readonly definition: RuntimeFunctionDefinition;
|
|
43
136
|
readonly args: FunctionArguments;
|
|
44
137
|
readonly schema: RuntimeSchemaDefinition;
|
|
138
|
+
readonly environment?: Readonly<EnvironmentType<DefinedEnv>>;
|
|
45
139
|
}
|
|
46
140
|
|
|
47
141
|
export class FunctionRegistry extends Context.Service<
|
|
@@ -59,6 +153,7 @@ export class FunctionRegistry extends Context.Service<
|
|
|
59
153
|
FunctionRegistry,
|
|
60
154
|
Effect.gen(function* () {
|
|
61
155
|
const path = yield* Path.Path;
|
|
156
|
+
const developmentEnvironment = yield* DevelopmentEnvironment;
|
|
62
157
|
return FunctionRegistry.of({
|
|
63
158
|
resolve: Effect.fn("FunctionRegistry.resolve")(function* (functionAddress, kind, args) {
|
|
64
159
|
const { functionName, moduleName } = apiFunctionParts(functionAddress);
|
|
@@ -84,6 +179,14 @@ export class FunctionRegistry extends Context.Service<
|
|
|
84
179
|
});
|
|
85
180
|
}
|
|
86
181
|
const definition = inspected.definition;
|
|
182
|
+
const environmentDefinition = getFunctionEnvironment(candidate);
|
|
183
|
+
if (environmentDefinition === undefined) {
|
|
184
|
+
return yield* FunctionUnavailable.make({
|
|
185
|
+
cause: new Error(`${functionAddress} has no environment binding.`),
|
|
186
|
+
function: functionAddress,
|
|
187
|
+
message: `Could not load ${functionAddress}.`,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
87
190
|
|
|
88
191
|
if (definition._tag !== kind) {
|
|
89
192
|
return yield* WrongFunctionKind.make({
|
|
@@ -105,7 +208,22 @@ export class FunctionRegistry extends Context.Service<
|
|
|
105
208
|
),
|
|
106
209
|
);
|
|
107
210
|
|
|
108
|
-
|
|
211
|
+
const loadedEnvironment = yield* developmentEnvironment.snapshot.pipe(
|
|
212
|
+
Effect.mapError((cause) =>
|
|
213
|
+
FunctionUnavailable.make({
|
|
214
|
+
cause,
|
|
215
|
+
function: functionAddress,
|
|
216
|
+
message: `Could not load ${functionAddress}.`,
|
|
217
|
+
}),
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
args: decodedArgs,
|
|
223
|
+
definition,
|
|
224
|
+
environment: loadedEnvironment.value,
|
|
225
|
+
schema: inspected.schema,
|
|
226
|
+
};
|
|
109
227
|
}),
|
|
110
228
|
});
|
|
111
229
|
}),
|
|
@@ -166,7 +284,12 @@ export class FunctionExecutor extends Context.Service<
|
|
|
166
284
|
return FunctionExecutor.of({
|
|
167
285
|
execute: Effect.fn("FunctionExecutor.execute")(function* (functionAddress, kind, resolved) {
|
|
168
286
|
const invoke = (context: RuntimeHandlerContext) =>
|
|
169
|
-
Effect.gen(() =>
|
|
287
|
+
Effect.gen(() =>
|
|
288
|
+
resolved.definition.handler(
|
|
289
|
+
Object.freeze({ ...context, env: resolved.environment ?? Object.freeze({}) }),
|
|
290
|
+
resolved.args,
|
|
291
|
+
),
|
|
292
|
+
).pipe(
|
|
170
293
|
Effect.matchEffect({
|
|
171
294
|
onFailure: (error) => encodeFailure(resolved.definition, error),
|
|
172
295
|
onSuccess: (value) => encodeSuccess(resolved.definition, value),
|