ignotum 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/cli/bin.mjs +295 -64
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/runtime/{api-BTeMI5tx.js → api-DzcR7spt.js} +22 -23
- package/dist/runtime/api-DzcR7spt.js.map +1 -0
- package/dist/runtime/{api-D9lV-5av.d.ts → api-jCl8Hzry.d.ts} +3 -3
- package/dist/runtime/client.d.ts +5 -5
- package/dist/runtime/client.js +57 -55
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/{descriptor-C5VA9qRl-C338iC6l.js → descriptor-C5VA9qRl-BhD-WQeO.js} +4 -3
- package/dist/runtime/descriptor-C5VA9qRl-BhD-WQeO.js.map +1 -0
- package/dist/runtime/{file-BXf63ulU.js → file-C1abuMgd.js} +8 -1
- package/dist/runtime/file-C1abuMgd.js.map +1 -0
- package/dist/runtime/{id-Btwac71X-DGj0DQuu.d.ts → id-Cs82tq9Q-Caqfx54f.d.ts} +2 -2
- package/dist/runtime/{index-CF04_Dps.d.ts → index-D6VLTbDB.d.ts} +4 -4
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/api.js +1 -1
- package/dist/runtime/internal/host.d.ts +4 -4
- package/dist/runtime/internal/host.js +3 -3
- 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-DrOowBve-Bnipd34u.d.ts → pagination-Bt3l7QaC-D_zI5zsu.d.ts} +3 -3
- package/dist/runtime/pagination-DcIkTOFs.d.ts +1 -0
- package/dist/runtime/{schema-DJfwfq87.js → schema-C00wobuy.js} +3 -3
- package/dist/runtime/{schema-DJfwfq87.js.map → schema-C00wobuy.js.map} +1 -1
- package/dist/runtime/server.d.ts +2 -2
- package/dist/runtime/server.js +2 -2
- package/dist/runtime/{sync-mIXn9eKv.d.ts → sync-bMQq9tXx.d.ts} +2 -2
- package/package.json +4 -2
- package/src/cli/auth-client.ts +286 -0
- package/src/cli/command.ts +65 -2
- package/src/cli/control-client.ts +19 -6
- package/src/cli/new-app.ts +2 -1
- package/src/client/errors.ts +7 -10
- package/src/client/files.ts +36 -51
- package/src/client/sync.ts +35 -23
- package/src/dev-runtime/files.ts +30 -31
- package/src/dev-runtime/sync.ts +19 -25
- package/dist/runtime/api-BTeMI5tx.js.map +0 -1
- package/dist/runtime/descriptor-C5VA9qRl-C338iC6l.js.map +0 -1
- package/dist/runtime/file-BXf63ulU.js.map +0 -1
- package/dist/runtime/pagination-BNFhAjns.d.ts +0 -1
package/src/cli/command.ts
CHANGED
|
@@ -6,6 +6,12 @@ import { IdGenerator } from "@ignotum/shared/id";
|
|
|
6
6
|
|
|
7
7
|
import packageJson from "../../package.json" with { type: "json" };
|
|
8
8
|
import { generate } from "./codegen.js";
|
|
9
|
+
import {
|
|
10
|
+
AuthCredentialStore,
|
|
11
|
+
login as runLogin,
|
|
12
|
+
logout as runLogout,
|
|
13
|
+
status as readAuthStatus,
|
|
14
|
+
} from "./auth-client.js";
|
|
9
15
|
import { ControlClient } from "./control-client.js";
|
|
10
16
|
import { deploy as runDeploy } from "./deploy.js";
|
|
11
17
|
import { resetDevDatabase } from "../dev-runtime/dev-database.js";
|
|
@@ -15,6 +21,58 @@ import { installDependencies } from "./package-manager.js";
|
|
|
15
21
|
|
|
16
22
|
const DevPort = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65_535 }));
|
|
17
23
|
|
|
24
|
+
const authLogin = Command.make(
|
|
25
|
+
"login",
|
|
26
|
+
{},
|
|
27
|
+
Effect.fn("auth login")(function* () {
|
|
28
|
+
const terminal = yield* Terminal.Terminal;
|
|
29
|
+
const result = yield* runLogin().pipe(
|
|
30
|
+
Effect.mapError((error) =>
|
|
31
|
+
CliError.UserError.make({ cause: error, userMessage: error.message }),
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
yield* terminal.display(`Signed in as ${result.email}.\n`);
|
|
35
|
+
}),
|
|
36
|
+
).pipe(Command.withDescription("Sign in to Ignotum with GitHub."));
|
|
37
|
+
|
|
38
|
+
const authLogout = Command.make(
|
|
39
|
+
"logout",
|
|
40
|
+
{},
|
|
41
|
+
Effect.fn("auth logout")(function* () {
|
|
42
|
+
const terminal = yield* Terminal.Terminal;
|
|
43
|
+
const removed = yield* runLogout().pipe(
|
|
44
|
+
Effect.mapError((error) =>
|
|
45
|
+
CliError.UserError.make({ cause: error, userMessage: error.message }),
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
yield* terminal.display(removed ? "Signed out.\n" : "Not signed in.\n");
|
|
49
|
+
}),
|
|
50
|
+
).pipe(Command.withDescription("Revoke and remove the saved login."));
|
|
51
|
+
|
|
52
|
+
const authStatus = Command.make(
|
|
53
|
+
"status",
|
|
54
|
+
{},
|
|
55
|
+
Effect.fn("auth status")(function* () {
|
|
56
|
+
const terminal = yield* Terminal.Terminal;
|
|
57
|
+
const current = yield* readAuthStatus().pipe(
|
|
58
|
+
Effect.mapError((error) =>
|
|
59
|
+
CliError.UserError.make({ cause: error, userMessage: error.message }),
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
if (current === undefined) {
|
|
63
|
+
yield* terminal.display("Not signed in.\n");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const team = current.teamName === undefined ? "" : `\nTeam: ${current.teamName}`;
|
|
67
|
+
yield* terminal.display(`Signed in as ${current.name} (${current.email}).${team}\n`);
|
|
68
|
+
}),
|
|
69
|
+
).pipe(Command.withDescription("Show the current Ignotum login."));
|
|
70
|
+
|
|
71
|
+
const auth = Command.make("auth").pipe(
|
|
72
|
+
Command.withDescription("Manage your Ignotum login."),
|
|
73
|
+
Command.withSubcommands([authLogin, authLogout, authStatus]),
|
|
74
|
+
);
|
|
75
|
+
|
|
18
76
|
const codegen = Command.make(
|
|
19
77
|
"codegen",
|
|
20
78
|
{},
|
|
@@ -172,7 +230,7 @@ const newApp = Command.make(
|
|
|
172
230
|
);
|
|
173
231
|
|
|
174
232
|
const ignotum = Command.make("ignotum").pipe(
|
|
175
|
-
Command.withSubcommands([newApp, install, codegen, dev, deploy]),
|
|
233
|
+
Command.withSubcommands([newApp, install, codegen, dev, deploy, auth]),
|
|
176
234
|
);
|
|
177
235
|
|
|
178
236
|
export const main = (): void => {
|
|
@@ -180,7 +238,12 @@ export const main = (): void => {
|
|
|
180
238
|
Command.run({ version: packageJson.version }),
|
|
181
239
|
// @effect-diagnostics-next-line strictEffectProvide:off
|
|
182
240
|
Effect.provide(
|
|
183
|
-
Layer.mergeAll(
|
|
241
|
+
Layer.mergeAll(
|
|
242
|
+
NodeServices.layer,
|
|
243
|
+
NodeHttpClient.layerUndici,
|
|
244
|
+
IdGenerator.layer,
|
|
245
|
+
AuthCredentialStore.layer.pipe(Layer.provide(NodeServices.layer)),
|
|
246
|
+
),
|
|
184
247
|
),
|
|
185
248
|
NodeRuntime.runMain,
|
|
186
249
|
);
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
} from "effect";
|
|
42
42
|
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
|
|
43
43
|
import type { HttpClientRequest as HttpRequest } from "effect/unstable/http/HttpClientRequest";
|
|
44
|
+
import { AuthCommandError, AuthCredentialStore } from "./auth-client.js";
|
|
44
45
|
|
|
45
46
|
export interface HostedControlConfig {
|
|
46
47
|
readonly apiUrl: URL;
|
|
@@ -69,11 +70,19 @@ export class HostedControlConfiguration extends Context.Service<
|
|
|
69
70
|
>()("ignotum/cli/control-client/HostedControlConfiguration") {
|
|
70
71
|
static readonly layer = Layer.effect(
|
|
71
72
|
HostedControlConfiguration,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
Effect.gen(function* () {
|
|
74
|
+
const credential = yield* (yield* AuthCredentialStore).load;
|
|
75
|
+
if (credential === undefined) {
|
|
76
|
+
return yield* AuthCommandError.make({
|
|
77
|
+
message: "Run 'npx ignotum auth login' first.",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
apiUrl: yield* Config.schema(HostedApiUrl, "IGNOTUM_API_URL").pipe(
|
|
82
|
+
Config.withDefault(new URL("https://api.ignotum.cloud")),
|
|
83
|
+
),
|
|
84
|
+
token: credential.token,
|
|
85
|
+
};
|
|
77
86
|
}),
|
|
78
87
|
);
|
|
79
88
|
}
|
|
@@ -133,7 +142,11 @@ export class ControlClient extends Context.Service<ControlClient, ControlClientS
|
|
|
133
142
|
"ignotum/cli/control-client/ControlClient",
|
|
134
143
|
) {
|
|
135
144
|
static get layer() {
|
|
136
|
-
return controlClientLayer.pipe(
|
|
145
|
+
return controlClientLayer.pipe(
|
|
146
|
+
Layer.provideMerge(
|
|
147
|
+
HostedControlConfiguration.layer.pipe(Layer.provide(AuthCredentialStore.layer)),
|
|
148
|
+
),
|
|
149
|
+
);
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
|
package/src/cli/new-app.ts
CHANGED
|
@@ -209,9 +209,10 @@ and updates them when the schema or server functions change.
|
|
|
209
209
|
|
|
210
210
|
## Deploy
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
Sign in, then choose a unique slug on the first deployment:
|
|
213
213
|
|
|
214
214
|
\`\`\`sh
|
|
215
|
+
npx ignotum auth login
|
|
215
216
|
npx ignotum deploy --app my-app
|
|
216
217
|
\`\`\`
|
|
217
218
|
|
package/src/client/errors.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** @effect-diagnostics globalConsole:skip-file */
|
|
2
2
|
import { Cause, Effect, ErrorReporter, Schema } from "effect";
|
|
3
3
|
|
|
4
|
-
import { FunctionAddress, Operation
|
|
4
|
+
import { ErrorCode, FunctionAddress, Operation } from "@ignotum/contracts/runtime/sync";
|
|
5
5
|
|
|
6
6
|
export class ConnectionUnavailable extends Schema.TaggedError<ConnectionUnavailable>()(
|
|
7
7
|
"ConnectionUnavailable",
|
|
@@ -36,21 +36,18 @@ export class InvalidServerMessage extends Schema.TaggedError<InvalidServerMessag
|
|
|
36
36
|
},
|
|
37
37
|
) {}
|
|
38
38
|
|
|
39
|
-
export class
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
operation: Schema.optional(Operation),
|
|
45
|
-
},
|
|
46
|
-
) {}
|
|
39
|
+
export class ServerError extends Schema.TaggedError<ServerError>()("ServerError", {
|
|
40
|
+
code: ErrorCode,
|
|
41
|
+
message: Schema.String,
|
|
42
|
+
operation: Schema.optional(Operation),
|
|
43
|
+
}) {}
|
|
47
44
|
|
|
48
45
|
export const ClientInfrastructureError = Schema.Union([
|
|
49
46
|
ConnectionUnavailable,
|
|
50
47
|
InvalidClientMessage,
|
|
51
48
|
InvalidMutationArguments,
|
|
52
49
|
InvalidServerMessage,
|
|
53
|
-
|
|
50
|
+
ServerError,
|
|
54
51
|
]);
|
|
55
52
|
export type ClientInfrastructureError = typeof ClientInfrastructureError.Type;
|
|
56
53
|
|
package/src/client/files.ts
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
makeFileValue,
|
|
11
11
|
type FileFormat,
|
|
12
12
|
type FileValue,
|
|
13
|
-
type RuntimeFileOccurrence,
|
|
14
13
|
} from "@ignotum/contracts/schema/file";
|
|
15
14
|
import { IdGenerator } from "@ignotum/shared/id";
|
|
16
15
|
import { Effect, Predicate } from "effect";
|
|
@@ -28,30 +27,22 @@ export const Files = {
|
|
|
28
27
|
const formatByMime: Partial<Record<string, FileFormat>> = {};
|
|
29
28
|
for (const format of fileFormats) formatByMime[fileMimeTypes[format]] = format;
|
|
30
29
|
|
|
31
|
-
interface NativeOccurrence {
|
|
32
|
-
readonly path: ReadonlyArray<string | number>;
|
|
33
|
-
readonly value: File;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
30
|
const collectNativeFiles = (
|
|
37
31
|
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- This is the recursive parser for generated mutation arguments and native File values.
|
|
38
32
|
value: unknown,
|
|
39
|
-
|
|
40
|
-
occurrences: Array<NativeOccurrence>,
|
|
33
|
+
files: Array<File>,
|
|
41
34
|
): void => {
|
|
42
35
|
if (value instanceof File) {
|
|
43
|
-
|
|
36
|
+
files.push(value);
|
|
44
37
|
return;
|
|
45
38
|
}
|
|
46
39
|
if (isFileValue(value) || Predicate.isDate(value)) return;
|
|
47
40
|
if (globalThis.Array.isArray(value)) {
|
|
48
|
-
for (const
|
|
49
|
-
collectNativeFiles(child, [...path, index], occurrences);
|
|
41
|
+
for (const child of value) collectNativeFiles(child, files);
|
|
50
42
|
return;
|
|
51
43
|
}
|
|
52
44
|
if (!Predicate.isObject(value)) return;
|
|
53
|
-
for (const
|
|
54
|
-
collectNativeFiles(child, [...path, key], occurrences);
|
|
45
|
+
for (const child of Object.values(value)) collectNativeFiles(child, files);
|
|
55
46
|
};
|
|
56
47
|
|
|
57
48
|
const replaceNativeFiles = (
|
|
@@ -73,7 +64,7 @@ const replaceNativeFiles = (
|
|
|
73
64
|
export interface PreparedMutationArguments {
|
|
74
65
|
readonly value: unknown;
|
|
75
66
|
readonly nativeFiles: ReadonlyMap<FileId, File>;
|
|
76
|
-
readonly
|
|
67
|
+
readonly fileIds: ReadonlyArray<FileId>;
|
|
77
68
|
}
|
|
78
69
|
|
|
79
70
|
export const prepareMutationArguments = Effect.fn("SyncClient.prepareMutationArguments")(function* (
|
|
@@ -81,9 +72,9 @@ export const prepareMutationArguments = Effect.fn("SyncClient.prepareMutationArg
|
|
|
81
72
|
input: unknown,
|
|
82
73
|
) {
|
|
83
74
|
const ids = yield* IdGenerator;
|
|
84
|
-
const native:
|
|
85
|
-
collectNativeFiles(input,
|
|
86
|
-
const unique = new Set(native
|
|
75
|
+
const native: File[] = [];
|
|
76
|
+
collectNativeFiles(input, native);
|
|
77
|
+
const unique = new Set(native);
|
|
87
78
|
if (unique.size > fileLimits.filesPerMutation) {
|
|
88
79
|
throw new Error(`A mutation may upload at most ${fileLimits.filesPerMutation} files.`);
|
|
89
80
|
}
|
|
@@ -111,58 +102,52 @@ export const prepareMutationArguments = Effect.fn("SyncClient.prepareMutationArg
|
|
|
111
102
|
nativeFiles.set(id, file);
|
|
112
103
|
}
|
|
113
104
|
const value = replaceNativeFiles(input, replacements);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
file: {
|
|
120
|
-
id: fileIdOf(replacement),
|
|
121
|
-
format: replacement.format,
|
|
122
|
-
name: replacement.name,
|
|
123
|
-
size: replacement.size,
|
|
124
|
-
},
|
|
125
|
-
} satisfies RuntimeFileOccurrence;
|
|
126
|
-
});
|
|
127
|
-
return { value, nativeFiles, occurrences } satisfies PreparedMutationArguments;
|
|
105
|
+
return {
|
|
106
|
+
value,
|
|
107
|
+
nativeFiles,
|
|
108
|
+
fileIds: Array.from(nativeFiles.keys()),
|
|
109
|
+
} satisfies PreparedMutationArguments;
|
|
128
110
|
});
|
|
129
111
|
|
|
130
|
-
interface
|
|
112
|
+
interface FileGrant {
|
|
113
|
+
readonly id: FileId;
|
|
131
114
|
readonly url: string;
|
|
132
115
|
}
|
|
133
116
|
|
|
134
|
-
const pathKey = (path: ReadonlyArray<string | number>): string => JSON.stringify(path);
|
|
135
|
-
|
|
136
117
|
// @effect-diagnostics-next-line missingPipeableSignature:off File grant attachment naturally takes the decoded value first.
|
|
137
118
|
export const applyFileGrants = (
|
|
138
|
-
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Query result types are erased at the wire decoding boundary and checked against grant
|
|
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.
|
|
139
120
|
value: unknown,
|
|
140
|
-
grants: ReadonlyArray<
|
|
121
|
+
grants: ReadonlyArray<FileGrant>,
|
|
141
122
|
// oxlint-disable-next-line anti-slop/no-unknown-returns -- The traversal preserves the decoded query result shape while attaching private symbols.
|
|
142
123
|
): unknown => {
|
|
143
|
-
const
|
|
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>();
|
|
144
130
|
const visit = (
|
|
145
|
-
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- This recursive visitor
|
|
131
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- This recursive visitor identifies file values before attaching grants.
|
|
146
132
|
current: unknown,
|
|
147
|
-
path: ReadonlyArray<string | number>,
|
|
148
133
|
// oxlint-disable-next-line anti-slop/no-unknown-returns -- Recursive branches preserve their input shape.
|
|
149
134
|
): unknown => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return grantFileValue(current,
|
|
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);
|
|
156
141
|
}
|
|
157
142
|
if (globalThis.Array.isArray(current)) {
|
|
158
|
-
return current.map(
|
|
143
|
+
return current.map(visit);
|
|
159
144
|
}
|
|
160
|
-
if (!Predicate.isObject(current) || Predicate.isDate(current)
|
|
145
|
+
if (!Predicate.isObject(current) || Predicate.isDate(current)) {
|
|
161
146
|
return current;
|
|
162
147
|
}
|
|
163
|
-
return Object.fromEntries(
|
|
164
|
-
Object.entries(current).map(([key, child]) => [key, visit(child, [...path, key])]),
|
|
165
|
-
);
|
|
148
|
+
return Object.fromEntries(Object.entries(current).map(([key, child]) => [key, visit(child)]));
|
|
166
149
|
};
|
|
167
|
-
|
|
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;
|
|
168
153
|
};
|
package/src/client/sync.ts
CHANGED
|
@@ -26,7 +26,7 @@ import type {
|
|
|
26
26
|
import { failureFromWire, pending, Result } from "@ignotum/contracts/runtime/result";
|
|
27
27
|
import {
|
|
28
28
|
ClientMessageJson,
|
|
29
|
-
|
|
29
|
+
DeploymentCloseCode,
|
|
30
30
|
InvocationId,
|
|
31
31
|
ServerMessageJson,
|
|
32
32
|
SubscriptionId,
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
ConnectionUnavailable,
|
|
43
43
|
InvalidClientMessage,
|
|
44
44
|
InvalidServerMessage,
|
|
45
|
-
|
|
45
|
+
ServerError,
|
|
46
46
|
InvalidMutationArguments,
|
|
47
47
|
clientErrorReporterLayer,
|
|
48
48
|
reportClientError,
|
|
@@ -213,8 +213,7 @@ const resultFromWire = Effect.fn("SyncClient.resultFromWire")(
|
|
|
213
213
|
),
|
|
214
214
|
);
|
|
215
215
|
|
|
216
|
-
export const shouldReloadForCloseCode = (code: number): boolean =>
|
|
217
|
-
code === DeploymentChangedCloseCode;
|
|
216
|
+
export const shouldReloadForCloseCode = (code: number): boolean => code === DeploymentCloseCode;
|
|
218
217
|
|
|
219
218
|
const handshakesMatch = (first: SyncHandshake, next: SyncHandshake): boolean =>
|
|
220
219
|
first.appId === next.appId &&
|
|
@@ -265,9 +264,9 @@ interface PendingInvocation {
|
|
|
265
264
|
ClientInfrastructureError
|
|
266
265
|
>;
|
|
267
266
|
readonly function: FunctionAddress;
|
|
268
|
-
readonly message: Extract<ClientMessage, { readonly type: "Invoke" | "
|
|
267
|
+
readonly message: Extract<ClientMessage, { readonly type: "Invoke" | "Prepare" }>;
|
|
269
268
|
readonly prepared: Deferred.Deferred<
|
|
270
|
-
Extract<ServerMessage, { readonly type: "
|
|
269
|
+
Extract<ServerMessage, { readonly type: "Preparation" }>["uploads"],
|
|
271
270
|
ClientInfrastructureError
|
|
272
271
|
>;
|
|
273
272
|
}
|
|
@@ -339,7 +338,7 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
339
338
|
};
|
|
340
339
|
|
|
341
340
|
const handleMessage = Effect.fn("SyncClient.handleMessage")(function* (
|
|
342
|
-
message: Exclude<ServerMessage, SyncHandshake | { readonly type: "
|
|
341
|
+
message: Exclude<ServerMessage, SyncHandshake | { readonly type: "Deployment" }>,
|
|
343
342
|
) {
|
|
344
343
|
switch (message.type) {
|
|
345
344
|
case "Snapshot": {
|
|
@@ -352,7 +351,7 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
352
351
|
);
|
|
353
352
|
return;
|
|
354
353
|
}
|
|
355
|
-
case "
|
|
354
|
+
case "Preparation": {
|
|
356
355
|
const pendingInvocation = HashMap.getUnsafe(invocations, message.id);
|
|
357
356
|
if (pendingInvocation !== undefined) {
|
|
358
357
|
yield* Deferred.succeed(pendingInvocation.prepared, message.uploads);
|
|
@@ -368,14 +367,14 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
368
367
|
);
|
|
369
368
|
return;
|
|
370
369
|
}
|
|
371
|
-
case "
|
|
370
|
+
case "Error": {
|
|
372
371
|
const error =
|
|
373
372
|
message.operation === undefined
|
|
374
|
-
?
|
|
373
|
+
? ServerError.make({
|
|
375
374
|
code: message.code,
|
|
376
375
|
message: message.message,
|
|
377
376
|
})
|
|
378
|
-
:
|
|
377
|
+
: ServerError.make({
|
|
379
378
|
code: message.code,
|
|
380
379
|
message: message.message,
|
|
381
380
|
operation: message.operation,
|
|
@@ -449,7 +448,7 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
449
448
|
),
|
|
450
449
|
);
|
|
451
450
|
if (message.type === "Handshake") return yield* acceptHandshake(message);
|
|
452
|
-
if (message.type === "
|
|
451
|
+
if (message.type === "Deployment") {
|
|
453
452
|
requestReload();
|
|
454
453
|
return;
|
|
455
454
|
}
|
|
@@ -534,7 +533,7 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
534
533
|
ClientInfrastructureError
|
|
535
534
|
>();
|
|
536
535
|
const prepared = yield* Deferred.make<
|
|
537
|
-
Extract<ServerMessage, { readonly type: "
|
|
536
|
+
Extract<ServerMessage, { readonly type: "Preparation" }>["uploads"],
|
|
538
537
|
ClientInfrastructureError
|
|
539
538
|
>();
|
|
540
539
|
const invokeMessage: Extract<ClientMessage, { readonly type: "Invoke" }> = {
|
|
@@ -545,15 +544,15 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
545
544
|
args: jsonArgs,
|
|
546
545
|
};
|
|
547
546
|
const message: PendingInvocation["message"] =
|
|
548
|
-
preparedArguments.
|
|
547
|
+
preparedArguments.fileIds.length === 0
|
|
549
548
|
? invokeMessage
|
|
550
549
|
: {
|
|
551
|
-
type: "
|
|
550
|
+
type: "Prepare",
|
|
552
551
|
id: invocationId,
|
|
553
552
|
kind: "Mutation",
|
|
554
553
|
function: functionAddress,
|
|
555
554
|
args: jsonArgs,
|
|
556
|
-
files: preparedArguments.
|
|
555
|
+
files: preparedArguments.fileIds,
|
|
557
556
|
};
|
|
558
557
|
invocations = HashMap.set(invocations, invocationId, {
|
|
559
558
|
deferred,
|
|
@@ -570,20 +569,27 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
570
569
|
}),
|
|
571
570
|
);
|
|
572
571
|
}
|
|
573
|
-
if (message.type === "
|
|
572
|
+
if (message.type === "Prepare") {
|
|
574
573
|
const uploads = yield* Deferred.await(prepared);
|
|
575
|
-
const
|
|
574
|
+
const expected = new Set(message.files);
|
|
575
|
+
const received = new Set<string>();
|
|
576
576
|
for (const upload of uploads) {
|
|
577
|
-
if (upload.
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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);
|
|
581
585
|
if (file === undefined) {
|
|
582
586
|
return yield* InvalidServerMessage.make({
|
|
583
|
-
cause: new Error(`Missing native file '${upload.
|
|
587
|
+
cause: new Error(`Missing native file '${upload.id}'.`),
|
|
584
588
|
message: "The server prepared an unknown application file.",
|
|
585
589
|
});
|
|
586
590
|
}
|
|
591
|
+
if (upload.url === undefined) continue;
|
|
592
|
+
const uploadUrl = upload.url;
|
|
587
593
|
const response = yield* Effect.tryPromise({
|
|
588
594
|
try: () =>
|
|
589
595
|
fetch(uploadUrl, {
|
|
@@ -606,6 +612,12 @@ export class SyncClient extends Context.Service<SyncClient, SyncClientService>()
|
|
|
606
612
|
});
|
|
607
613
|
}
|
|
608
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
|
+
}
|
|
609
621
|
const pendingInvocation = HashMap.getUnsafe(invocations, invocationId);
|
|
610
622
|
if (pendingInvocation === undefined) return yield* Deferred.await(deferred);
|
|
611
623
|
invocations = HashMap.set(invocations, invocationId, {
|
package/src/dev-runtime/files.ts
CHANGED
|
@@ -45,8 +45,8 @@ interface LocalApplicationFilesService {
|
|
|
45
45
|
invocationId: InvocationId,
|
|
46
46
|
functionAddress: FunctionAddress,
|
|
47
47
|
args: Schema.Json,
|
|
48
|
-
|
|
49
|
-
) => Effect.Effect<ReadonlyArray<
|
|
48
|
+
fileIds: ReadonlyArray<FileId>,
|
|
49
|
+
) => Effect.Effect<ReadonlyArray<{ readonly id: FileId; readonly url?: string }>>;
|
|
50
50
|
readonly admit: (
|
|
51
51
|
invocationId: InvocationId,
|
|
52
52
|
functionAddress: FunctionAddress,
|
|
@@ -56,7 +56,7 @@ interface LocalApplicationFilesService {
|
|
|
56
56
|
readonly grants: (
|
|
57
57
|
subscriptionId: SubscriptionId,
|
|
58
58
|
files: ReadonlyArray<RuntimeFileOccurrenceValue>,
|
|
59
|
-
) => Effect.Effect<ReadonlyArray<
|
|
59
|
+
) => Effect.Effect<ReadonlyArray<{ readonly id: FileId; readonly url: string }>>;
|
|
60
60
|
readonly releaseGrants: (subscriptionId: SubscriptionId) => Effect.Effect<void>;
|
|
61
61
|
readonly readGrant: (
|
|
62
62
|
token: FileGrantToken,
|
|
@@ -74,8 +74,11 @@ export const LocalApplicationFiles = Context.Reference<LocalApplicationFilesServ
|
|
|
74
74
|
admit: (_invocationId, _functionAddress, args) =>
|
|
75
75
|
Effect.succeed(encodedFileOccurrencesOf(args)),
|
|
76
76
|
grants: (_subscriptionId, files) =>
|
|
77
|
-
Effect.succeed(
|
|
78
|
-
|
|
77
|
+
Effect.succeed(
|
|
78
|
+
Array.from(new Set(files.map(({ file }) => file.id)), (id) => ({ id, url: "" })),
|
|
79
|
+
),
|
|
80
|
+
prepare: (_invocationId, _functionAddress, _args, fileIds) =>
|
|
81
|
+
Effect.succeed(fileIds.map((id) => ({ id }))),
|
|
79
82
|
upload: () => Effect.die("Local application file storage is unavailable."),
|
|
80
83
|
releaseGrants: () => Effect.void,
|
|
81
84
|
readGrant: () => Effect.succeed(Option.none()),
|
|
@@ -125,8 +128,19 @@ export const localApplicationFilesLayer = (appDirectory: string) =>
|
|
|
125
128
|
invocationId: InvocationId,
|
|
126
129
|
functionAddress: FunctionAddress,
|
|
127
130
|
args: Schema.Json,
|
|
128
|
-
|
|
131
|
+
fileIds: ReadonlyArray<FileId>,
|
|
129
132
|
) {
|
|
133
|
+
if (fileIds.length === 0) throw new Error("A file preparation must contain a file ID.");
|
|
134
|
+
const selected = new Set(fileIds);
|
|
135
|
+
if (selected.size !== fileIds.length) {
|
|
136
|
+
throw new Error("A file preparation contains a duplicate file ID.");
|
|
137
|
+
}
|
|
138
|
+
const files = encodedFileOccurrencesOf(args).filter((occurrence) =>
|
|
139
|
+
selected.has(occurrence.file.id),
|
|
140
|
+
);
|
|
141
|
+
if (new Set(files.map((occurrence) => occurrence.file.id)).size !== selected.size) {
|
|
142
|
+
throw new Error("Prepared files do not match the mutation arguments.");
|
|
143
|
+
}
|
|
130
144
|
const existing = preparations.get(invocationId);
|
|
131
145
|
if (existing !== undefined) {
|
|
132
146
|
if (
|
|
@@ -136,27 +150,14 @@ export const localApplicationFilesLayer = (appDirectory: string) =>
|
|
|
136
150
|
) {
|
|
137
151
|
throw new Error("The invocation ID already belongs to another file preparation.");
|
|
138
152
|
}
|
|
139
|
-
return
|
|
140
|
-
const token = existing.uploads.get(
|
|
141
|
-
return token === undefined
|
|
142
|
-
? occurrence
|
|
143
|
-
: { ...occurrence, url: `${fileUploadUrlPrefix}${token}` };
|
|
153
|
+
return fileIds.map((id) => {
|
|
154
|
+
const token = existing.uploads.get(id);
|
|
155
|
+
return token === undefined ? { id } : { id, url: `${fileUploadUrlPrefix}${token}` };
|
|
144
156
|
});
|
|
145
157
|
}
|
|
146
|
-
const all = new Map(
|
|
147
|
-
encodedFileOccurrencesOf(args).map((occurrence) => [
|
|
148
|
-
encodeCanonicalJson(occurrence.path),
|
|
149
|
-
occurrence.file,
|
|
150
|
-
]),
|
|
151
|
-
);
|
|
152
158
|
const distinct = new Map<FileId, RuntimeFileMetadata>();
|
|
153
159
|
for (const occurrence of files) {
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
arg === undefined ||
|
|
157
|
-
!sameFile(arg, occurrence.file) ||
|
|
158
|
-
!validFilename(occurrence.file.name)
|
|
159
|
-
) {
|
|
160
|
+
if (!validFilename(occurrence.file.name)) {
|
|
160
161
|
throw new Error("Prepared files do not match the mutation arguments.");
|
|
161
162
|
}
|
|
162
163
|
const prior = distinct.get(occurrence.file.id);
|
|
@@ -214,11 +215,9 @@ export const localApplicationFilesLayer = (appDirectory: string) =>
|
|
|
214
215
|
`;
|
|
215
216
|
}
|
|
216
217
|
preparations.set(invocationId, { function: functionAddress, args, files, uploads });
|
|
217
|
-
return
|
|
218
|
-
const token = uploads.get(
|
|
219
|
-
return token === undefined
|
|
220
|
-
? occurrence
|
|
221
|
-
: { ...occurrence, url: `${fileUploadUrlPrefix}${token}` };
|
|
218
|
+
return fileIds.map((id) => {
|
|
219
|
+
const token = uploads.get(id);
|
|
220
|
+
return token === undefined ? { id } : { id, url: `${fileUploadUrlPrefix}${token}` };
|
|
222
221
|
});
|
|
223
222
|
});
|
|
224
223
|
|
|
@@ -279,9 +278,9 @@ export const localApplicationFilesLayer = (appDirectory: string) =>
|
|
|
279
278
|
tokens.set(occurrence.file.id, token);
|
|
280
279
|
grants.set(token, { subscriptionId, file: occurrence.file });
|
|
281
280
|
}
|
|
282
|
-
return
|
|
283
|
-
|
|
284
|
-
url: `${fileGrantUrlPrefix}${
|
|
281
|
+
return Array.from(tokens, ([id, token]) => ({
|
|
282
|
+
id,
|
|
283
|
+
url: `${fileGrantUrlPrefix}${token}`,
|
|
285
284
|
}));
|
|
286
285
|
});
|
|
287
286
|
|