ignotum 0.0.7 → 0.0.9

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 (58) hide show
  1. package/README.md +89 -101
  2. package/dist/cli/bin.mjs +1119 -195
  3. package/dist/cli/bin.mjs.map +1 -1
  4. package/dist/runtime/{api-DtX8qPrq.js → api-DzcR7spt.js} +36 -13
  5. package/dist/runtime/api-DzcR7spt.js.map +1 -0
  6. package/dist/runtime/{api-Dp4J-xt-.d.ts → api-jCl8Hzry.d.ts} +7 -9
  7. package/dist/runtime/client.d.ts +12 -5
  8. package/dist/runtime/client.js +285 -60
  9. package/dist/runtime/client.js.map +1 -1
  10. package/dist/runtime/{descriptor-t6BOEGw9-C1rwYIlx.js → descriptor-C5VA9qRl-BKbdenuU.js} +38 -7
  11. package/dist/runtime/descriptor-C5VA9qRl-BKbdenuU.js.map +1 -0
  12. package/dist/runtime/file-C1abuMgd.js +173 -0
  13. package/dist/runtime/file-C1abuMgd.js.map +1 -0
  14. package/dist/runtime/{id-Btwac71X-DhnKYsjY.d.ts → id-Cs82tq9Q-Caqfx54f.d.ts} +51 -5
  15. package/dist/runtime/{index-B5KSOjGN.d.ts → index-D6VLTbDB.d.ts} +40 -21
  16. package/dist/runtime/internal/api.d.ts +2 -2
  17. package/dist/runtime/internal/api.js +1 -1
  18. package/dist/runtime/internal/host.d.ts +8 -7
  19. package/dist/runtime/internal/host.js +21 -10
  20. package/dist/runtime/internal/host.js.map +1 -1
  21. package/dist/runtime/internal/server.d.ts +1 -1
  22. package/dist/runtime/internal/server.js +1 -1
  23. package/dist/runtime/internal/types.d.ts +1 -1
  24. package/dist/runtime/internal/types.js +1 -1
  25. package/dist/runtime/{pagination-B1BzNkh8-BUSTbeSg.d.ts → pagination-Bt3l7QaC-D_zI5zsu.d.ts} +8 -4
  26. package/dist/runtime/pagination-DcIkTOFs.d.ts +1 -0
  27. package/dist/runtime/{schema-D9RmboaS.js → schema-B6PK_ZwV.js} +17 -3
  28. package/dist/runtime/schema-B6PK_ZwV.js.map +1 -0
  29. package/dist/runtime/server.d.ts +3 -3
  30. package/dist/runtime/server.js +2 -2
  31. package/dist/runtime/server.js.map +1 -1
  32. package/dist/runtime/sync-bMQq9tXx.d.ts +8 -0
  33. package/package.json +7 -5
  34. package/src/cli/agent-files.ts +52 -13
  35. package/src/cli/app-configuration.ts +4 -1
  36. package/src/cli/auth-client.ts +286 -0
  37. package/src/cli/build/server.ts +83 -6
  38. package/src/cli/command.ts +65 -2
  39. package/src/cli/control-client.ts +19 -6
  40. package/src/cli/new-app.ts +16 -0
  41. package/src/client/errors.ts +7 -10
  42. package/src/client/files.ts +153 -0
  43. package/src/client/hooks.ts +2 -15
  44. package/src/client/index.ts +7 -0
  45. package/src/client/sync.ts +158 -30
  46. package/src/dev-runtime/database.ts +69 -57
  47. package/src/dev-runtime/files.ts +337 -0
  48. package/src/dev-runtime/functions.ts +14 -6
  49. package/src/dev-runtime/migrations.ts +14 -0
  50. package/src/dev-runtime/sync.ts +134 -22
  51. package/src/internal/api.ts +16 -1
  52. package/src/server/index.ts +8 -1
  53. package/dist/runtime/api-DtX8qPrq.js.map +0 -1
  54. package/dist/runtime/descriptor-t6BOEGw9-C1rwYIlx.js.map +0 -1
  55. package/dist/runtime/id-D570vudg.js +0 -26
  56. package/dist/runtime/id-D570vudg.js.map +0 -1
  57. package/dist/runtime/pagination-BKPko9Hm.d.ts +0 -1
  58. package/dist/runtime/schema-D9RmboaS.js.map +0 -1
@@ -0,0 +1,153 @@
1
+ import {
2
+ FileId,
3
+ fileGrantUrlOf,
4
+ fileIdOf,
5
+ fileFormats,
6
+ fileLimits,
7
+ fileMimeTypes,
8
+ grantFileValue,
9
+ isFileValue,
10
+ makeFileValue,
11
+ type FileFormat,
12
+ type FileValue,
13
+ } from "@ignotum/contracts/schema/file";
14
+ import { IdGenerator } from "@ignotum/shared/id";
15
+ import { Effect, Predicate } from "effect";
16
+
17
+ export const Files = {
18
+ url: (file: FileValue): string => {
19
+ const url = fileGrantUrlOf(file);
20
+ if (url === undefined) {
21
+ throw new Error("This FileValue has no active query grant.");
22
+ }
23
+ return url;
24
+ },
25
+ } as const;
26
+
27
+ const formatByMime: Partial<Record<string, FileFormat>> = {};
28
+ for (const format of fileFormats) formatByMime[fileMimeTypes[format]] = format;
29
+
30
+ const collectNativeFiles = (
31
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- This is the recursive parser for generated mutation arguments and native File values.
32
+ value: unknown,
33
+ files: Array<File>,
34
+ ): void => {
35
+ if (value instanceof File) {
36
+ files.push(value);
37
+ return;
38
+ }
39
+ if (isFileValue(value) || Predicate.isDate(value)) return;
40
+ if (globalThis.Array.isArray(value)) {
41
+ for (const child of value) collectNativeFiles(child, files);
42
+ return;
43
+ }
44
+ if (!Predicate.isObject(value)) return;
45
+ for (const child of Object.values(value)) collectNativeFiles(child, files);
46
+ };
47
+
48
+ const replaceNativeFiles = (
49
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Each node is classified before it is copied into the transport value.
50
+ value: unknown,
51
+ replacements: ReadonlyMap<File, FileValue>,
52
+ // oxlint-disable-next-line anti-slop/no-unknown-returns -- The generated function schema validates the reconstructed argument value on the server.
53
+ ): unknown => {
54
+ if (value instanceof File) return replacements.get(value);
55
+ if (isFileValue(value) || Predicate.isDate(value)) return value;
56
+ if (globalThis.Array.isArray(value))
57
+ return value.map((child) => replaceNativeFiles(child, replacements));
58
+ if (!Predicate.isObject(value)) return value;
59
+ return Object.fromEntries(
60
+ Object.entries(value).map(([key, child]) => [key, replaceNativeFiles(child, replacements)]),
61
+ );
62
+ };
63
+
64
+ export interface PreparedMutationArguments {
65
+ readonly value: unknown;
66
+ readonly nativeFiles: ReadonlyMap<FileId, File>;
67
+ readonly fileIds: ReadonlyArray<FileId>;
68
+ }
69
+
70
+ export const prepareMutationArguments = Effect.fn("SyncClient.prepareMutationArguments")(function* (
71
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Generated mutation argument types are erased only at this transport preparation boundary.
72
+ input: unknown,
73
+ ) {
74
+ const ids = yield* IdGenerator;
75
+ const native: File[] = [];
76
+ collectNativeFiles(input, native);
77
+ const unique = new Set(native);
78
+ if (unique.size > fileLimits.filesPerMutation) {
79
+ throw new Error(`A mutation may upload at most ${fileLimits.filesPerMutation} files.`);
80
+ }
81
+ const totalBytes = [...unique].reduce((total, file) => total + file.size, 0);
82
+ if (totalBytes > fileLimits.mutationBytes) {
83
+ throw new Error(`Mutation uploads may total at most ${fileLimits.mutationBytes} bytes.`);
84
+ }
85
+ const replacements = new Map<File, FileValue>();
86
+ const nativeFiles = new Map<FileId, File>();
87
+ for (const file of unique) {
88
+ const format = formatByMime[file.type.toLowerCase()];
89
+ if (format === undefined) {
90
+ throw new Error(
91
+ `File '${file.name}' has unsupported media type '${file.type || "unknown"}'.`,
92
+ );
93
+ }
94
+ if (file.size === 0 || file.size > fileLimits.fileBytes) {
95
+ throw new Error(`File '${file.name}' must be between 1 and ${fileLimits.fileBytes} bytes.`);
96
+ }
97
+ if (new TextEncoder().encode(file.name).byteLength > fileLimits.filenameBytes) {
98
+ throw new Error(`File '${file.name}' has a filename that is too long.`);
99
+ }
100
+ const id = yield* ids.generate(FileId);
101
+ replacements.set(file, makeFileValue(id, { format, name: file.name, size: file.size }));
102
+ nativeFiles.set(id, file);
103
+ }
104
+ const value = replaceNativeFiles(input, replacements);
105
+ return {
106
+ value,
107
+ nativeFiles,
108
+ fileIds: Array.from(nativeFiles.keys()),
109
+ } satisfies PreparedMutationArguments;
110
+ });
111
+
112
+ interface FileGrant {
113
+ readonly id: FileId;
114
+ readonly url: string;
115
+ }
116
+
117
+ // @effect-diagnostics-next-line missingPipeableSignature:off File grant attachment naturally takes the decoded value first.
118
+ export const applyFileGrants = (
119
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Query result types are erased at the wire decoding boundary and checked against grant identities below.
120
+ value: unknown,
121
+ grants: ReadonlyArray<FileGrant>,
122
+ // oxlint-disable-next-line anti-slop/no-unknown-returns -- The traversal preserves the decoded query result shape while attaching private symbols.
123
+ ): unknown => {
124
+ const byId = new Map<FileId, string>();
125
+ for (const grant of grants) {
126
+ if (byId.has(grant.id)) throw new Error("A query file grant ID is duplicated.");
127
+ byId.set(grant.id, grant.url);
128
+ }
129
+ const matched = new Set<FileId>();
130
+ const visit = (
131
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- This recursive visitor identifies file values before attaching grants.
132
+ current: unknown,
133
+ // oxlint-disable-next-line anti-slop/no-unknown-returns -- Recursive branches preserve their input shape.
134
+ ): unknown => {
135
+ if (isFileValue(current)) {
136
+ const id = fileIdOf(current);
137
+ const url = byId.get(id);
138
+ if (url === undefined) return current;
139
+ matched.add(id);
140
+ return grantFileValue(current, url);
141
+ }
142
+ if (globalThis.Array.isArray(current)) {
143
+ return current.map(visit);
144
+ }
145
+ if (!Predicate.isObject(current) || Predicate.isDate(current)) {
146
+ return current;
147
+ }
148
+ return Object.fromEntries(Object.entries(current).map(([key, child]) => [key, visit(child)]));
149
+ };
150
+ const granted = visit(value);
151
+ if (matched.size !== byId.size) throw new Error("A query file grant has no result value.");
152
+ return granted;
153
+ };
@@ -10,11 +10,7 @@ import {
10
10
  type PaginationPage,
11
11
  } from "@ignotum/contracts/runtime/pagination";
12
12
  import { encodeTransportObject } from "@ignotum/contracts/runtime/sync";
13
- import {
14
- ClientInfrastructureError,
15
- InvalidMutationArguments,
16
- reportClientError,
17
- } from "./errors.js";
13
+ import { ClientInfrastructureError } from "./errors.js";
18
14
  import { isQuerySkip, type QuerySkip } from "./query.js";
19
15
  import { SyncClient, syncClientInternals, syncRuntime } from "./sync.js";
20
16
 
@@ -278,19 +274,10 @@ export const useMutation = <Args extends object | void, Success, Failure extends
278
274
  return syncRuntime.runPromise<SettledResult<Success, Failure>, ClientInfrastructureError>(
279
275
  Effect.gen(function* () {
280
276
  const client = yield* SyncClient;
281
- const jsonArgs = yield* Effect.try({
282
- try: () => encodeTransportObject(input),
283
- catch: (cause) =>
284
- InvalidMutationArguments.make({
285
- cause,
286
- function: functionPath,
287
- message: `Mutation arguments for ${functionPath} contain an unsupported value.`,
288
- }),
289
- }).pipe(Effect.tapError(reportClientError));
290
277
  // SAFETY: generated function references bind the runtime path to the
291
278
  // declared public result types validated by the server executor.
292
279
  // oxlint-disable-next-line anti-slop/no-chained-type-assertions -- The untyped sync transport deliberately erases the generated reference's result parameters.
293
- return (yield* client.mutate(functionPath, jsonArgs)) as unknown as SettledResult<
280
+ return (yield* client.mutate(functionPath, input)) as unknown as SettledResult<
294
281
  Success,
295
282
  Failure
296
283
  >;
@@ -9,6 +9,13 @@ export const Result = { match: contractResult.match };
9
9
  export type Result<Value, Error extends ErrorValue> = SettledResult<Value, Error>;
10
10
  export type QueryResult<Value, Error extends ErrorValue> = ContractQueryResult<Value, Error>;
11
11
  export type { InternalServerError } from "@ignotum/contracts/runtime/result";
12
+ export type {
13
+ FileFormat,
14
+ FileMetadata,
15
+ FileMimeType,
16
+ FileValue,
17
+ } from "@ignotum/contracts/schema/file";
18
+ export { Files } from "./files.js";
12
19
  export { useMutation, usePaginatedQuery, useQuery } from "./hooks.js";
13
20
  export type { PaginatedQueryOptions, PaginatedQueryValue, PaginationStatus } from "./hooks.js";
14
21
  export { Query } from "./query.js";
@@ -16,6 +16,7 @@ import * as BrowserCrypto from "@effect/platform-browser/BrowserCrypto";
16
16
  import { encodeCanonicalJson } from "@ignotum/contracts/json";
17
17
  import { type AppStateRevision } from "@ignotum/contracts/runtime/hosted";
18
18
  import { IdGenerator } from "@ignotum/shared/id";
19
+ import { encodeTransportObject } from "@ignotum/contracts/runtime/sync";
19
20
 
20
21
  import type {
21
22
  ErrorValue,
@@ -25,7 +26,7 @@ import type {
25
26
  import { failureFromWire, pending, Result } from "@ignotum/contracts/runtime/result";
26
27
  import {
27
28
  ClientMessageJson,
28
- DeploymentChangedCloseCode,
29
+ DeploymentCloseCode,
29
30
  InvocationId,
30
31
  ServerMessageJson,
31
32
  SubscriptionId,
@@ -41,11 +42,13 @@ import {
41
42
  ConnectionUnavailable,
42
43
  InvalidClientMessage,
43
44
  InvalidServerMessage,
44
- ServerProtocolError,
45
+ ServerError,
46
+ InvalidMutationArguments,
45
47
  clientErrorReporterLayer,
46
48
  reportClientError,
47
49
  } from "./errors.js";
48
50
  import { syncPath } from "../internal/http-paths.js";
51
+ import { applyFileGrants, prepareMutationArguments } from "./files.js";
49
52
 
50
53
  type Listener = () => void;
51
54
  type SocketWriter = (chunk: string) => Effect.Effect<void, Socket.SocketError>;
@@ -195,18 +198,22 @@ const makeQueryCache = (
195
198
  } satisfies QueryCache;
196
199
  };
197
200
 
198
- const resultFromWire = Effect.fn("SyncClient.resultFromWire")((wire: WireResult) =>
199
- Effect.succeed(
200
- wire.type === "Success"
201
- ? Result.succeed(
202
- wire.value === undefined ? undefined : decodeTransportValue(wire.value, wire.dates ?? []),
203
- )
204
- : failureFromWire(wire.error, wire.dates),
205
- ),
201
+ type SnapshotFiles = NonNullable<Extract<ServerMessage, { readonly type: "Snapshot" }>["files"]>;
202
+
203
+ const resultFromWire = Effect.fn("SyncClient.resultFromWire")(
204
+ (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
+ ),
206
214
  );
207
215
 
208
- export const shouldReloadForCloseCode = (code: number): boolean =>
209
- code === DeploymentChangedCloseCode;
216
+ export const shouldReloadForCloseCode = (code: number): boolean => code === DeploymentCloseCode;
210
217
 
211
218
  const handshakesMatch = (first: SyncHandshake, next: SyncHandshake): boolean =>
212
219
  first.appId === next.appId &&
@@ -242,7 +249,8 @@ const socketUrl = (): string => {
242
249
  interface SyncClientService {
243
250
  readonly mutate: (
244
251
  functionAddress: FunctionAddress,
245
- args: Schema.Json,
252
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Generated reference argument types are deliberately erased at the client transport boundary.
253
+ args: unknown,
246
254
  ) => Effect.Effect<IgnotumResult<unknown, ErrorValue>, ClientInfrastructureError>;
247
255
  readonly observe: (
248
256
  functionAddress: FunctionAddress,
@@ -256,7 +264,11 @@ interface PendingInvocation {
256
264
  ClientInfrastructureError
257
265
  >;
258
266
  readonly function: FunctionAddress;
259
- readonly message: Extract<ClientMessage, { readonly type: "Invoke" }>;
267
+ readonly message: Extract<ClientMessage, { readonly type: "Invoke" | "Prepare" }>;
268
+ readonly prepared: Deferred.Deferred<
269
+ Extract<ServerMessage, { readonly type: "Preparation" }>["uploads"],
270
+ ClientInfrastructureError
271
+ >;
260
272
  }
261
273
 
262
274
  export class SyncClient extends Context.Service<SyncClient, SyncClientService>()(
@@ -319,25 +331,33 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
319
331
  }
320
332
  invocations = HashMap.remove(invocations, id);
321
333
  return reportClientError(error).pipe(
334
+ Effect.andThen(Deferred.fail(pendingInvocation.prepared, error)),
322
335
  Effect.andThen(Deferred.fail(pendingInvocation.deferred, error)),
323
336
  Effect.asVoid,
324
337
  );
325
338
  };
326
339
 
327
340
  const handleMessage = Effect.fn("SyncClient.handleMessage")(function* (
328
- message: Exclude<ServerMessage, SyncHandshake | { readonly type: "DeploymentChanged" }>,
341
+ message: Exclude<ServerMessage, SyncHandshake | { readonly type: "Deployment" }>,
329
342
  ) {
330
343
  switch (message.type) {
331
344
  case "Snapshot": {
332
345
  const entry = queryCache.getById(message.id);
333
346
  if (entry === undefined) return;
334
- yield* resultFromWire(message.result).pipe(
347
+ yield* resultFromWire(message.result, message.files ?? []).pipe(
335
348
  Effect.tap((result) =>
336
349
  Effect.sync(() => queryCache.setResult(message.id, result, message.revision)),
337
350
  ),
338
351
  );
339
352
  return;
340
353
  }
354
+ case "Preparation": {
355
+ const pendingInvocation = HashMap.getUnsafe(invocations, message.id);
356
+ if (pendingInvocation !== undefined) {
357
+ yield* Deferred.succeed(pendingInvocation.prepared, message.uploads);
358
+ }
359
+ return;
360
+ }
341
361
  case "Result": {
342
362
  const pendingInvocation = HashMap.getUnsafe(invocations, message.id);
343
363
  if (pendingInvocation === undefined) return;
@@ -347,14 +367,14 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
347
367
  );
348
368
  return;
349
369
  }
350
- case "ProtocolError": {
370
+ case "Error": {
351
371
  const error =
352
372
  message.operation === undefined
353
- ? ServerProtocolError.make({
373
+ ? ServerError.make({
354
374
  code: message.code,
355
375
  message: message.message,
356
376
  })
357
- : ServerProtocolError.make({
377
+ : ServerError.make({
358
378
  code: message.code,
359
379
  message: message.message,
360
380
  operation: message.operation,
@@ -428,7 +448,7 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
428
448
  ),
429
449
  );
430
450
  if (message.type === "Handshake") return yield* acceptHandshake(message);
431
- if (message.type === "DeploymentChanged") {
451
+ if (message.type === "Deployment") {
432
452
  requestReload();
433
453
  return;
434
454
  }
@@ -488,31 +508,139 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
488
508
  const mutate: SyncClientService["mutate"] = Effect.fn("SyncClient.mutate")(
489
509
  function* (functionAddress, args) {
490
510
  const invocationId = yield* ids.generate(InvocationId);
511
+ const preparedArguments = yield* prepareMutationArguments(args).pipe(
512
+ Effect.provideService(IdGenerator, ids),
513
+ Effect.mapError((cause) =>
514
+ InvalidMutationArguments.make({
515
+ cause,
516
+ function: functionAddress,
517
+ message: `Mutation arguments for ${functionAddress} contain an unsupported file or value.`,
518
+ }),
519
+ ),
520
+ Effect.tapError(reportClientError),
521
+ );
522
+ const jsonArgs = yield* Effect.try({
523
+ try: () => encodeTransportObject(preparedArguments.value),
524
+ catch: (cause) =>
525
+ InvalidMutationArguments.make({
526
+ cause,
527
+ function: functionAddress,
528
+ message: `Mutation arguments for ${functionAddress} contain an unsupported value.`,
529
+ }),
530
+ }).pipe(Effect.tapError(reportClientError));
491
531
  const deferred = yield* Deferred.make<
492
532
  IgnotumResult<unknown, ErrorValue>,
493
533
  ClientInfrastructureError
494
534
  >();
495
- const message: ClientMessage = {
535
+ const prepared = yield* Deferred.make<
536
+ Extract<ServerMessage, { readonly type: "Preparation" }>["uploads"],
537
+ ClientInfrastructureError
538
+ >();
539
+ const invokeMessage: Extract<ClientMessage, { readonly type: "Invoke" }> = {
496
540
  type: "Invoke",
497
541
  id: invocationId,
498
542
  kind: "Mutation",
499
543
  function: functionAddress,
500
- args,
544
+ args: jsonArgs,
501
545
  };
546
+ const message: PendingInvocation["message"] =
547
+ preparedArguments.fileIds.length === 0
548
+ ? invokeMessage
549
+ : {
550
+ type: "Prepare",
551
+ id: invocationId,
552
+ kind: "Mutation",
553
+ function: functionAddress,
554
+ args: jsonArgs,
555
+ files: preparedArguments.fileIds,
556
+ };
502
557
  invocations = HashMap.set(invocations, invocationId, {
503
558
  deferred,
504
559
  function: functionAddress,
505
560
  message,
561
+ prepared,
506
562
  });
507
- if (writer !== undefined) {
508
- yield* sendWith(writer, message).pipe(
509
- Effect.catchTags({
510
- ConnectionUnavailable: reportClientError,
511
- InvalidClientMessage: (error) => rejectInvocation(invocationId, error),
563
+ return yield* Effect.gen(function* () {
564
+ if (writer !== undefined) {
565
+ yield* sendWith(writer, message).pipe(
566
+ Effect.catchTags({
567
+ ConnectionUnavailable: reportClientError,
568
+ InvalidClientMessage: (error) => rejectInvocation(invocationId, error),
569
+ }),
570
+ );
571
+ }
572
+ if (message.type === "Prepare") {
573
+ const uploads = yield* Deferred.await(prepared);
574
+ const expected = new Set(message.files);
575
+ const received = new Set<string>();
576
+ for (const upload of uploads) {
577
+ if (!expected.has(upload.id) || received.has(upload.id)) {
578
+ return yield* InvalidServerMessage.make({
579
+ cause: new Error(`Unexpected prepared file '${upload.id}'.`),
580
+ message: "The server returned invalid application file preparation data.",
581
+ });
582
+ }
583
+ received.add(upload.id);
584
+ const file = preparedArguments.nativeFiles.get(upload.id);
585
+ if (file === undefined) {
586
+ return yield* InvalidServerMessage.make({
587
+ cause: new Error(`Missing native file '${upload.id}'.`),
588
+ message: "The server prepared an unknown application file.",
589
+ });
590
+ }
591
+ 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
+ }
614
+ }
615
+ if (received.size !== expected.size) {
616
+ return yield* InvalidServerMessage.make({
617
+ cause: new Error("The server omitted a prepared file."),
618
+ message: "The server returned incomplete application file preparation data.",
619
+ });
620
+ }
621
+ const pendingInvocation = HashMap.getUnsafe(invocations, invocationId);
622
+ if (pendingInvocation === undefined) return yield* Deferred.await(deferred);
623
+ invocations = HashMap.set(invocations, invocationId, {
624
+ ...pendingInvocation,
625
+ message: invokeMessage,
626
+ });
627
+ if (writer !== undefined) {
628
+ yield* sendWith(writer, invokeMessage).pipe(
629
+ Effect.catchTags({
630
+ ConnectionUnavailable: reportClientError,
631
+ InvalidClientMessage: (error) => rejectInvocation(invocationId, error),
632
+ }),
633
+ );
634
+ }
635
+ }
636
+ return yield* Deferred.await(deferred);
637
+ }).pipe(
638
+ Effect.tapError(() =>
639
+ Effect.sync(() => {
640
+ invocations = HashMap.remove(invocations, invocationId);
512
641
  }),
513
- );
514
- }
515
- return yield* Deferred.await(deferred);
642
+ ),
643
+ );
516
644
  },
517
645
  );
518
646