ignotum 0.0.11 → 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.
Files changed (48) hide show
  1. package/README.md +8 -3
  2. package/dist/cli/bin.mjs +560 -102
  3. package/dist/cli/bin.mjs.map +1 -1
  4. package/dist/runtime/{api-DR_8vfKg.d.ts → api-5XSrIeqW.d.ts} +4 -3
  5. package/dist/runtime/client.d.ts +6 -5
  6. package/dist/runtime/client.js +170 -75
  7. package/dist/runtime/client.js.map +1 -1
  8. package/dist/runtime/{descriptor-C5VA9qRl-DuQsowaQ.js → descriptor-XzDX2JDw-j3O6YHgW.js} +23 -4
  9. package/dist/runtime/descriptor-XzDX2JDw-j3O6YHgW.js.map +1 -0
  10. package/dist/runtime/{id-Cs82tq9Q-hmYFyqTa.d.ts → id-Cs82tq9Q-CK-maMgN.d.ts} +12 -12
  11. package/dist/runtime/{index-2q9FwJud.d.ts → index-CrWg4Z0y.d.ts} +132 -76
  12. package/dist/runtime/internal/api.d.ts +1 -1
  13. package/dist/runtime/internal/host.d.ts +18 -17
  14. package/dist/runtime/internal/host.js +10 -5
  15. package/dist/runtime/internal/host.js.map +1 -1
  16. package/dist/runtime/internal/server.d.ts +1 -1
  17. package/dist/runtime/internal/server.js +1 -1
  18. package/dist/runtime/internal/types.d.ts +1 -1
  19. package/dist/runtime/internal/types.js +1 -1
  20. package/dist/runtime/pagination-CX5IPbEi.d.ts +1 -0
  21. package/dist/runtime/{pagination-Bt3l7QaC-BYEGmLBE.d.ts → pagination-D-R9NR61-CpIoHFoI.d.ts} +8 -5
  22. package/dist/runtime/result-DIjKM-p4.d.ts +1 -0
  23. package/dist/runtime/{schema-ERFjT8-m.js → schema-B9XxyRO8.js} +132 -16
  24. package/dist/runtime/schema-B9XxyRO8.js.map +1 -0
  25. package/dist/runtime/server.d.ts +7 -3
  26. package/dist/runtime/server.js +5 -3
  27. package/dist/runtime/server.js.map +1 -1
  28. package/dist/runtime/{sync-a2EdGSdY.d.ts → sync-avN7NkcU.d.ts} +2 -2
  29. package/package.json +4 -4
  30. package/src/cli/agent-files.ts +2 -0
  31. package/src/cli/auth-client.ts +1 -1
  32. package/src/cli/bin.ts +13 -3
  33. package/src/cli/build/server.ts +102 -13
  34. package/src/cli/codegen.ts +7 -4
  35. package/src/cli/control-client.ts +30 -8
  36. package/src/cli/deploy.ts +7 -2
  37. package/src/cli/environment.ts +133 -0
  38. package/src/client/files.ts +43 -27
  39. package/src/client/hooks.ts +26 -17
  40. package/src/client/page-observers.ts +77 -0
  41. package/src/client/sync.ts +71 -59
  42. package/src/dev-runtime/functions.ts +127 -4
  43. package/src/dev-runtime/sync.ts +38 -9
  44. package/src/server/index.ts +11 -1
  45. package/dist/runtime/descriptor-C5VA9qRl-DuQsowaQ.js.map +0 -1
  46. package/dist/runtime/pagination-CFJ3xlAt.d.ts +0 -1
  47. package/dist/runtime/schema-ERFjT8-m.js.map +0 -1
  48. package/src/dev-runtime/id.ts +0 -3
@@ -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 releases: Array<() => void> = [];
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
- const observer = yield* client.observe(functionPath, pageArgs);
188
- if (!active) {
189
- yield* observer.release;
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
+ };
@@ -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: AppStateRevision,
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 queryKeysById = new Map<SubscriptionId, string>();
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
- queryKeysById.set(created.subscriptionId, key);
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
- queryKeysById.delete(observed.subscriptionId);
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
- setError: (id, error) => {
177
- const key = queryKeysById.get(id);
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 (entry.revision !== undefined && revision < entry.revision) {
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.succeed(
206
- wire.type === "Success"
207
- ? Result.succeed(
208
- wire.value === undefined
209
- ? undefined
210
- : applyFileGrants(decodeTransportValue(wire.value, wire.dates ?? []), files),
211
- )
212
- : failureFromWire(wire.error, wire.dates),
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) => Deferred.succeed(pendingInvocation.deferred, 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.setError(message.operation.id, error);
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
- const uploadUrl = upload.url;
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 { type RuntimeSchemaDefinition } from "@ignotum/contracts/runtime/schema";
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 { Cause, Context, Effect, Layer, Path, Predicate, Schema, Semaphore } from "effect";
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
- return { args: decodedArgs, definition, schema: inspected.schema };
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(() => resolved.definition.handler(context, resolved.args)).pipe(
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),