@samebase/alchemy-convex 0.5.0 → 0.6.0
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 +21 -16
- package/lib/index.d.ts +11 -8
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +234 -157
- package/lib/index.js.map +1 -1
- package/package.json +2 -3
- package/src/Deployment.ts +9 -2
- package/src/Project.ts +125 -88
- package/src/Push.ts +74 -46
- package/src/State.ts +278 -159
- package/src/index.ts +1 -1
- package/src/{component/_generated/api.ts → store/convex/_generated/api.d.ts} +9 -8
- package/src/store/convex/_generated/api.js +23 -0
- package/src/{component/_generated/server.ts → store/convex/_generated/server.d.ts} +18 -25
- package/src/store/convex/_generated/server.js +101 -0
- package/src/{component → store/convex}/state.ts +96 -46
- package/src/store/convex/version.ts +6 -0
- package/src/store/package.json +8 -0
- package/src/component/_generated/component.ts +0 -92
- package/src/component/convex.config.ts +0 -5
- /package/src/{component/_generated/dataModel.ts → store/convex/_generated/dataModel.d.ts} +0 -0
- /package/src/{component → store/convex}/schema.ts +0 -0
package/src/State.ts
CHANGED
|
@@ -1,33 +1,44 @@
|
|
|
1
|
-
// Alchemy state in Convex: `state: Convex.state()` in `Alchemy.Stack`.
|
|
1
|
+
// Alchemy state in Convex: `state: Convex.state({ team })` in `Alchemy.Stack`.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// `deployment:data:view`, a component mutation `deployment:data:write`, and a
|
|
10
|
-
// call without a key gets HTTP 403. See docs/design/convex-state.md.
|
|
3
|
+
// One project for each Convex team, `alchemy-state`, holds the state of
|
|
4
|
+
// every stack and stage of the team in its production deployment: the
|
|
5
|
+
// tables `resources (stack, stage, fqn, value)` and `outputs (stack, stage,
|
|
6
|
+
// value)`, served by the functions of src/store/convex. The store lives
|
|
7
|
+
// outside every resource that a stack manages, so a stack can create and
|
|
8
|
+
// destroy its own project. See docs/design/convex-state.md.
|
|
11
9
|
//
|
|
12
|
-
// Bootstrap
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
10
|
+
// Bootstrap, on the first call of the run: the team token (CONVEX_ACCESS_TOKEN,
|
|
11
|
+
// or the Convex CLI login) finds the project by name, or creates it; a deploy
|
|
12
|
+
// key of its production deployment that can run internal queries and
|
|
13
|
+
// mutations, and nothing else, is created for the run and deleted at its
|
|
14
|
+
// end; the `state:version` query says whether the deployment has the store
|
|
15
|
+
// of this version. Without one, or with an older one, the installed Convex
|
|
16
|
+
// CLI pushes src/store/convex to it, with a push key that is deleted after
|
|
17
|
+
// the push. The functions are internal, so only POST /api/function with such
|
|
18
|
+
// a key reaches them: a public function of a root app answers anyone who
|
|
19
|
+
// knows the deployment URL (measured). A key that stops working during a
|
|
20
|
+
// long run (it expires after two hours) is replaced once per call.
|
|
19
21
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
22
|
+
// Two first runs of a team can both create a project named `alchemy-state`.
|
|
23
|
+
// The run whose project is not the oldest deletes its own, before it writes
|
|
24
|
+
// anything, and uses the oldest: both runs end on one store.
|
|
25
|
+
//
|
|
26
|
+
// The function names are a wire contract: Samebase reads `stacks` and
|
|
27
|
+
// `getOutput` by name, and an older client calls an older set of names. A
|
|
28
|
+
// name is never renamed or removed. STORE_VERSION rises when a function
|
|
29
|
+
// changes in a way that an older client cannot use, and a client never
|
|
30
|
+
// pushes an older store over a newer one.
|
|
31
|
+
import { dirname, join } from "node:path";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
23
33
|
import * as Effect from "effect/Effect";
|
|
24
34
|
import * as Layer from "effect/Layer";
|
|
35
|
+
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
|
|
25
36
|
import * as Redacted from "effect/Redacted";
|
|
26
37
|
import * as Schedule from "effect/Schedule";
|
|
27
38
|
import * as Schema from "effect/Schema";
|
|
39
|
+
import * as Scope from "effect/Scope";
|
|
28
40
|
import {
|
|
29
41
|
encodeState,
|
|
30
|
-
InMemoryService,
|
|
31
42
|
type PersistedState,
|
|
32
43
|
type ReplacedResourceState,
|
|
33
44
|
reviveState,
|
|
@@ -36,22 +47,57 @@ import {
|
|
|
36
47
|
STATE_STORE_VERSION,
|
|
37
48
|
StateStoreError,
|
|
38
49
|
} from "alchemy/State";
|
|
39
|
-
import {
|
|
50
|
+
import { fromConfig } from "./Credentials.ts";
|
|
51
|
+
import { ProductionDeploymentNotFound } from "./Deployment.ts";
|
|
52
|
+
import {
|
|
53
|
+
absentAsUndefined,
|
|
54
|
+
ManagementApi,
|
|
55
|
+
ManagementApiLive,
|
|
56
|
+
type ManagementApiService,
|
|
57
|
+
retryIdempotentWrite,
|
|
58
|
+
} from "./ManagementApi.ts";
|
|
59
|
+
import {
|
|
60
|
+
createProject,
|
|
61
|
+
findProjectByName,
|
|
62
|
+
listProjectsByName,
|
|
63
|
+
prodDeploymentOf,
|
|
64
|
+
} from "./Project.ts";
|
|
65
|
+
import {
|
|
66
|
+
createDeployKey,
|
|
67
|
+
deleteDeployKey,
|
|
68
|
+
installedCli,
|
|
69
|
+
push,
|
|
70
|
+
PUSH_KEY_ACTIONS,
|
|
71
|
+
withPushKey,
|
|
72
|
+
} from "./Push.ts";
|
|
73
|
+
import { STORE_VERSION } from "./store/convex/version.ts";
|
|
40
74
|
|
|
41
|
-
/**
|
|
42
|
-
export const
|
|
75
|
+
/** The name of the state project in each team. */
|
|
76
|
+
export const STORE_PROJECT_NAME = "alchemy-state";
|
|
43
77
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
78
|
+
/**
|
|
79
|
+
* The actions of the key of one run: the internal queries and mutations of
|
|
80
|
+
* the store, nothing else. Measured: `deployment:data:view` and
|
|
81
|
+
* `deployment:data:write` do not run an internal function.
|
|
82
|
+
*/
|
|
83
|
+
export const RUN_KEY_ACTIONS = [
|
|
84
|
+
"deployment:functions:runInternalQueries",
|
|
85
|
+
"deployment:functions:runInternalMutations",
|
|
86
|
+
] as const;
|
|
46
87
|
|
|
47
|
-
/** The body of POST /api/function
|
|
88
|
+
/** The body of POST /api/function, with HTTP 200 for a function error too. */
|
|
48
89
|
const Reply = Schema.Union([
|
|
49
90
|
Schema.Struct({ status: Schema.Literal("success"), value: Schema.Unknown }),
|
|
50
91
|
Schema.Struct({ status: Schema.Literal("error"), errorMessage: Schema.String }),
|
|
51
92
|
]);
|
|
93
|
+
type Reply = typeof Reply.Type;
|
|
52
94
|
|
|
53
|
-
/** The
|
|
54
|
-
const
|
|
95
|
+
/** The error reply of /api/function for a function that the deployment does not have (recorded). */
|
|
96
|
+
const MISSING_FUNCTION = /Could not find function for '/;
|
|
97
|
+
|
|
98
|
+
/** HTTP 401 or 403 from the function API: the key is gone or expired (recorded: 403 without a key). */
|
|
99
|
+
const isUnauthorized = (error: StateStoreError) =>
|
|
100
|
+
error.http?.status === 401 || error.http?.status === 403;
|
|
55
101
|
|
|
56
102
|
const failure = (message: string) => new StateStoreError({ message: `Convex state: ${message}` });
|
|
57
103
|
|
|
@@ -99,58 +145,34 @@ const decode =
|
|
|
99
145
|
(value: unknown) =>
|
|
100
146
|
Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => failure(`bad ${what}`)));
|
|
101
147
|
|
|
102
|
-
/**
|
|
103
|
-
* The deployment URL of a deploy key. The Convex CLI makes the same unofficial
|
|
104
|
-
* call for every deploy key (convex 1.46 `cli/lib/deploymentSelection.js`).
|
|
105
|
-
*/
|
|
106
|
-
const urlForKey = (key: Redacted.Redacted<string>) =>
|
|
107
|
-
post(
|
|
108
|
-
"https://api.convex.dev/api/deployment/url_for_key",
|
|
109
|
-
{},
|
|
110
|
-
{ deployKey: Redacted.value(key) },
|
|
111
|
-
).pipe(Effect.flatMap(decode(Schema.String, "deployment URL")));
|
|
112
|
-
|
|
113
|
-
/** The Convex function API of one deployment, with an admin key. */
|
|
148
|
+
/** The function API of the store deployment, with a deploy key. */
|
|
114
149
|
export interface FunctionApi {
|
|
115
|
-
/**
|
|
150
|
+
/** POST /api/function, the call that `convex run` makes: the reply, which can be a function error. */
|
|
116
151
|
readonly call: (
|
|
117
152
|
path: string,
|
|
118
153
|
args: Record<string, string | undefined>,
|
|
119
|
-
|
|
120
|
-
) => Effect.Effect<unknown, StateStoreError>;
|
|
154
|
+
) => Effect.Effect<Reply, StateStoreError>;
|
|
121
155
|
}
|
|
122
156
|
|
|
123
157
|
export const makeFunctionApi = (url: string, key: Redacted.Redacted<string>): FunctionApi => ({
|
|
124
|
-
call: (path, args
|
|
158
|
+
call: (path, args) =>
|
|
125
159
|
post(
|
|
126
|
-
|
|
127
|
-
// of the root answers only through /api/query (measured).
|
|
128
|
-
`${url}/api/${componentPath === undefined ? "query" : "function"}`,
|
|
160
|
+
`${url}/api/function`,
|
|
129
161
|
{ Authorization: `Convex ${Redacted.value(key)}` },
|
|
130
|
-
{
|
|
131
|
-
).pipe(
|
|
132
|
-
Effect.flatMap(decode(Reply, `reply of ${path}`)),
|
|
133
|
-
Effect.flatMap((reply) =>
|
|
134
|
-
reply.status === "success"
|
|
135
|
-
? Effect.succeed(reply.value)
|
|
136
|
-
: // The message names the function and the cause, such as a missing key
|
|
137
|
-
// action. The key is a header, and Convex does not echo arguments.
|
|
138
|
-
Effect.fail(failure(reply.errorMessage.trim())),
|
|
139
|
-
),
|
|
140
|
-
),
|
|
162
|
+
{ path, args, format: "json" },
|
|
163
|
+
).pipe(Effect.flatMap(decode(Reply, `reply of ${path}`))),
|
|
141
164
|
});
|
|
142
165
|
|
|
143
|
-
/** The
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
Effect.
|
|
147
|
-
Effect.
|
|
148
|
-
);
|
|
166
|
+
/** The value of a reply, or the Convex message, which names the function and the cause, such as a missing key action. */
|
|
167
|
+
const valueOf = (reply: Reply) =>
|
|
168
|
+
reply.status === "success"
|
|
169
|
+
? Effect.succeed(reply.value)
|
|
170
|
+
: Effect.fail(failure(reply.errorMessage.trim()));
|
|
149
171
|
|
|
150
|
-
/** The
|
|
172
|
+
/** The functions of the store with Alchemy's encoding: the JSON text of encodeState. */
|
|
151
173
|
const remoteOf = (api: FunctionApi) => {
|
|
152
174
|
const run = (name: string, args: Record<string, string | undefined>) =>
|
|
153
|
-
api.call(`state:${name}`, args
|
|
175
|
+
api.call(`state:${name}`, args).pipe(Effect.flatMap(valueOf));
|
|
154
176
|
const strings = (name: string, args: Record<string, string>) =>
|
|
155
177
|
run(name, args).pipe(Effect.flatMap(decode(Schema.Array(Schema.String), name)));
|
|
156
178
|
const optional = (name: string, args: Record<string, string>) =>
|
|
@@ -189,127 +211,224 @@ const remoteOf = (api: FunctionApi) => {
|
|
|
189
211
|
};
|
|
190
212
|
};
|
|
191
213
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
214
|
+
/** The version that the store answers, or undefined when the deployment has no `state:version`. */
|
|
215
|
+
const storeVersion = (api: FunctionApi) =>
|
|
216
|
+
api
|
|
217
|
+
.call("state:version", {})
|
|
218
|
+
.pipe(
|
|
219
|
+
Effect.flatMap((reply) =>
|
|
220
|
+
reply.status === "error" && MISSING_FUNCTION.test(reply.errorMessage)
|
|
221
|
+
? Effect.succeed(undefined)
|
|
222
|
+
: valueOf(reply).pipe(Effect.flatMap(decode(Schema.Number, "store version"))),
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The functions directory of the store app, `src/store/convex` of this
|
|
228
|
+
* package. This file is `src/State.ts` in a checkout and a chunk in `lib/`
|
|
229
|
+
* in the installed package, which ships `src` too, so the directory is
|
|
230
|
+
* `../src/store/convex` from either place.
|
|
231
|
+
*/
|
|
232
|
+
export const storeDirectory = () =>
|
|
233
|
+
join(dirname(fileURLToPath(import.meta.url)), "..", "src", "store", "convex");
|
|
234
|
+
|
|
235
|
+
export interface StoreOptions {
|
|
236
|
+
/** Team slug or numeric team id of the `alchemy-state` project. */
|
|
237
|
+
readonly team: string | number;
|
|
238
|
+
readonly api: ManagementApiService;
|
|
239
|
+
readonly spawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
|
|
240
|
+
/** The key of the run is deleted when this scope closes. */
|
|
241
|
+
readonly scope: Scope.Scope;
|
|
242
|
+
/** The functions directory that an outdated store gets: {@link storeDirectory}. */
|
|
243
|
+
readonly store: string;
|
|
244
|
+
}
|
|
195
245
|
|
|
196
246
|
/**
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
247
|
+
* Pushes `store` to the production deployment of the state project with the
|
|
248
|
+
* installed Convex CLI: no typecheck, no codegen, the key only in the
|
|
249
|
+
* environment of the CLI.
|
|
200
250
|
*/
|
|
201
|
-
|
|
251
|
+
const pushStore = (
|
|
252
|
+
options: StoreOptions,
|
|
253
|
+
deployment: { readonly name: string; readonly deploymentUrl: string },
|
|
254
|
+
) =>
|
|
202
255
|
Effect.gen(function* () {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
256
|
+
const cli = yield* installedCli(join(options.store, ".."));
|
|
257
|
+
yield* Effect.logInfo(
|
|
258
|
+
`Convex state: pushing the store version ${STORE_VERSION} to ${deployment.name}`,
|
|
259
|
+
);
|
|
260
|
+
yield* withPushKey(options.api, deployment.name, PUSH_KEY_ACTIONS, (key) =>
|
|
261
|
+
push(options.spawner, key, {
|
|
262
|
+
deployment: deployment.name,
|
|
263
|
+
url: deployment.deploymentUrl,
|
|
264
|
+
source: {
|
|
265
|
+
path: options.store,
|
|
266
|
+
command: [...cli, "deploy", "--typecheck", "disable", "--codegen", "disable"],
|
|
267
|
+
},
|
|
268
|
+
env: {},
|
|
269
|
+
previousEnvNames: [],
|
|
270
|
+
adopt: false,
|
|
271
|
+
previewName: undefined,
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
});
|
|
207
275
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Creates the state project of the team. Two first runs can both create one:
|
|
278
|
+
* the run whose project is not the oldest deletes its own, which holds
|
|
279
|
+
* nothing yet, and uses the oldest, so that every run ends on one store.
|
|
280
|
+
*/
|
|
281
|
+
const createStoreProject = (api: ManagementApiService, teamId: number, team: string | number) =>
|
|
282
|
+
Effect.gen(function* () {
|
|
283
|
+
yield* Effect.logInfo(
|
|
284
|
+
`Convex state: creating the project ${STORE_PROJECT_NAME} in team ${team}`,
|
|
215
285
|
);
|
|
286
|
+
const created = yield* createProject(api, teamId, {
|
|
287
|
+
name: STORE_PROJECT_NAME,
|
|
288
|
+
deploymentType: "prod",
|
|
289
|
+
deploymentRegion: undefined,
|
|
290
|
+
});
|
|
291
|
+
const all = yield* listProjectsByName(api, teamId, STORE_PROJECT_NAME);
|
|
292
|
+
const oldest = all.reduce((a, b) => (b.id < a.id ? b : a), created);
|
|
293
|
+
if (oldest.id === created.id) return created;
|
|
294
|
+
yield* Effect.logInfo(
|
|
295
|
+
`Convex state: another run created ${STORE_PROJECT_NAME} ${oldest.id} first; project ${created.id} is deleted again.`,
|
|
296
|
+
);
|
|
297
|
+
yield* absentAsUndefined(
|
|
298
|
+
retryIdempotentWrite(
|
|
299
|
+
api.requestVoid("delete project", (client) =>
|
|
300
|
+
client.POST("/projects/{project_id}/delete", {
|
|
301
|
+
params: { path: { project_id: created.id } },
|
|
302
|
+
}),
|
|
303
|
+
),
|
|
304
|
+
),
|
|
305
|
+
);
|
|
306
|
+
return oldest;
|
|
307
|
+
});
|
|
216
308
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
309
|
+
/**
|
|
310
|
+
* The StateService over the `alchemy-state` project of the team: found or
|
|
311
|
+
* created, its store pushed when it is missing or older, and read and
|
|
312
|
+
* written with the keys of this run.
|
|
313
|
+
*/
|
|
314
|
+
export const makeConvexState = (options: StoreOptions) =>
|
|
315
|
+
Effect.gen(function* () {
|
|
316
|
+
const { api, team } = options;
|
|
317
|
+
const teamId = yield* api.resolveTeamId(team);
|
|
318
|
+
const found = yield* findProjectByName(api, teamId, STORE_PROJECT_NAME);
|
|
319
|
+
const project = found ?? (yield* createStoreProject(api, teamId, team));
|
|
320
|
+
const deployment = yield* prodDeploymentOf(api, project.id);
|
|
321
|
+
if (deployment === undefined) {
|
|
322
|
+
return yield* new ProductionDeploymentNotFound({ projectId: project.id });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// The keys of this run, deleted together when the scope closes. A call
|
|
326
|
+
// that the current key no longer authorizes gets one new key and one
|
|
327
|
+
// more attempt.
|
|
328
|
+
const keys: Redacted.Redacted<string>[] = [];
|
|
329
|
+
yield* Scope.addFinalizer(
|
|
330
|
+
options.scope,
|
|
331
|
+
Effect.suspend(() =>
|
|
332
|
+
Effect.forEach(keys, (key) => deleteDeployKey(api, deployment.name, key), {
|
|
333
|
+
discard: true,
|
|
334
|
+
}),
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
const mint = createDeployKey(api, deployment.name, "alchemy-state", RUN_KEY_ACTIONS).pipe(
|
|
338
|
+
Effect.map((key) => {
|
|
339
|
+
keys.push(key);
|
|
340
|
+
return makeFunctionApi(deployment.deploymentUrl, key);
|
|
341
|
+
}),
|
|
342
|
+
Effect.mapError((error) => failure(`no key for ${deployment.name}: ${error.message}`)),
|
|
343
|
+
);
|
|
344
|
+
let current = yield* mint;
|
|
345
|
+
const functions: FunctionApi = {
|
|
346
|
+
call: (path, args) =>
|
|
347
|
+
current.call(path, args).pipe(
|
|
348
|
+
Effect.catchIf(isUnauthorized, () =>
|
|
349
|
+
mint.pipe(
|
|
350
|
+
Effect.flatMap((next) => {
|
|
351
|
+
current = next;
|
|
352
|
+
return next.call(path, args);
|
|
353
|
+
}),
|
|
354
|
+
),
|
|
355
|
+
),
|
|
356
|
+
),
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const version = yield* storeVersion(functions);
|
|
360
|
+
if (version === undefined || version < STORE_VERSION) {
|
|
361
|
+
// Code without `state:version` is another app with the name of the store.
|
|
362
|
+
if (version === undefined && deployment.lastDeployTime) {
|
|
363
|
+
return yield* failure(
|
|
364
|
+
`project ${STORE_PROJECT_NAME} (${project.id}) of team ${team} has code that is not the state store, and Convex.state does not push over it. Rename that project in the Convex dashboard.`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
// Another run can push a newer store in the meantime: read again right
|
|
368
|
+
// before the push. The push itself has no lock (see the design note).
|
|
369
|
+
const now = yield* storeVersion(functions);
|
|
370
|
+
if (now === undefined || now < STORE_VERSION) {
|
|
371
|
+
yield* pushStore(options, deployment);
|
|
372
|
+
const pushed = yield* storeVersion(functions);
|
|
373
|
+
if (pushed === undefined || pushed < STORE_VERSION) {
|
|
231
374
|
return yield* failure(
|
|
232
|
-
`${
|
|
375
|
+
`${deployment.name} answers store version ${pushed ?? "none"} after the push of version ${STORE_VERSION}.`,
|
|
233
376
|
);
|
|
234
377
|
}
|
|
235
378
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
const byStage = outputs[stack];
|
|
241
|
-
if (byStage !== undefined && stage in byStage) {
|
|
242
|
-
yield* remote.setOutput({ stack, stage, value: byStage[stage] });
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
pending = false;
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
const read = <A>(
|
|
249
|
-
inMemory: Effect.Effect<A, StateStoreError>,
|
|
250
|
-
inConvex: Effect.Effect<A, StateStoreError>,
|
|
251
|
-
) => Effect.suspend(() => (pending ? inMemory : inConvex));
|
|
252
|
-
const write = <A>(
|
|
253
|
-
inMemory: Effect.Effect<A, StateStoreError>,
|
|
254
|
-
inConvex: Effect.Effect<unknown, StateStoreError>,
|
|
255
|
-
result: A,
|
|
256
|
-
) =>
|
|
257
|
-
Effect.suspend(() =>
|
|
258
|
-
pending ? Effect.tap(inMemory, () => flush) : Effect.as(inConvex, result),
|
|
379
|
+
} else if (version > STORE_VERSION) {
|
|
380
|
+
// The names are a wire contract, so a newer store serves this client.
|
|
381
|
+
yield* Effect.logInfo(
|
|
382
|
+
`Convex state: the store of team ${team} is version ${version}, newer than version ${STORE_VERSION} of this package, and it stays.`,
|
|
259
383
|
);
|
|
384
|
+
}
|
|
260
385
|
|
|
386
|
+
const remote = remoteOf(functions);
|
|
261
387
|
const service: StateService = {
|
|
262
388
|
id: "convex",
|
|
263
389
|
getVersion: () => Effect.succeed(STATE_STORE_VERSION),
|
|
264
|
-
listStacks:
|
|
265
|
-
listStages:
|
|
266
|
-
list:
|
|
267
|
-
get:
|
|
268
|
-
getReplacedResources:
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
write(memory.deleteStack(request), remote.deleteStack(request), undefined),
|
|
275
|
-
// Alchemy writes the stack output last, so state still in memory here is lost.
|
|
276
|
-
setOutput: (request) =>
|
|
277
|
-
write(memory.setOutput(request), remote.setOutput(request), request.value).pipe(
|
|
278
|
-
Effect.tap(() =>
|
|
279
|
-
pending
|
|
280
|
-
? Effect.fail(
|
|
281
|
-
failure(
|
|
282
|
-
`the deployment of ${STATE_DEPLOY_KEY_ENV} has no ${COMPONENT_PATH} component at the end of the run, so this run saved no state. Mount @samebase/alchemy-convex/convex.config in convex/convex.config.ts.`,
|
|
283
|
-
),
|
|
284
|
-
)
|
|
285
|
-
: Effect.void,
|
|
286
|
-
),
|
|
287
|
-
),
|
|
390
|
+
listStacks: remote.listStacks,
|
|
391
|
+
listStages: remote.listStages,
|
|
392
|
+
list: remote.list,
|
|
393
|
+
get: remote.get,
|
|
394
|
+
getReplacedResources: remote.getReplacedResources,
|
|
395
|
+
getOutput: remote.getOutput,
|
|
396
|
+
set: (request) => remote.set(request).pipe(Effect.as(request.value)),
|
|
397
|
+
delete: (request) => remote.delete(request).pipe(Effect.asVoid),
|
|
398
|
+
deleteStack: (request) => remote.deleteStack(request).pipe(Effect.asVoid),
|
|
399
|
+
setOutput: (request) => remote.setOutput(request).pipe(Effect.as(request.value)),
|
|
288
400
|
};
|
|
289
401
|
return service;
|
|
290
402
|
});
|
|
291
403
|
|
|
292
404
|
/**
|
|
293
|
-
* Alchemy state in the
|
|
294
|
-
*
|
|
295
|
-
* the project
|
|
296
|
-
* `
|
|
405
|
+
* Alchemy state in the `alchemy-state` project of `team`, with the team token
|
|
406
|
+
* in CONVEX_ACCESS_TOKEN or the Convex CLI login. The first use of the run
|
|
407
|
+
* finds or creates the project and pushes the store when it is missing or
|
|
408
|
+
* older. `alchemy plan` on another stack never touches Convex.
|
|
297
409
|
*/
|
|
298
|
-
export const state = (
|
|
410
|
+
export const state = (options: {
|
|
411
|
+
readonly team: string | number;
|
|
412
|
+
}): Layer.Layer<State, never, ChildProcessSpawner.ChildProcessSpawner> =>
|
|
299
413
|
Layer.effect(
|
|
300
414
|
State,
|
|
301
415
|
Effect.gen(function* () {
|
|
302
|
-
|
|
416
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
417
|
+
const scope = yield* Scope.Scope;
|
|
418
|
+
// The stack's ConfigProvider, for the read of the token on first use.
|
|
303
419
|
const context = yield* Effect.context<never>();
|
|
304
420
|
return yield* Effect.cached(
|
|
305
421
|
Effect.gen(function* () {
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
422
|
+
const api = yield* ManagementApi.pipe(
|
|
423
|
+
Effect.provide(ManagementApiLive().pipe(Layer.provide(fromConfig()))),
|
|
424
|
+
);
|
|
425
|
+
return yield* makeConvexState({
|
|
426
|
+
team: options.team,
|
|
427
|
+
api,
|
|
428
|
+
spawner,
|
|
429
|
+
scope,
|
|
430
|
+
store: storeDirectory(),
|
|
431
|
+
});
|
|
313
432
|
}).pipe(Effect.provideContext(context), Effect.orDie),
|
|
314
433
|
);
|
|
315
434
|
}),
|
package/src/index.ts
CHANGED
|
@@ -28,7 +28,7 @@ export {
|
|
|
28
28
|
ProjectTeamChange,
|
|
29
29
|
} from "./Project.ts";
|
|
30
30
|
export { Providers, providers } from "./Providers.ts";
|
|
31
|
-
export {
|
|
31
|
+
export { state, STORE_PROJECT_NAME } from "./State.ts";
|
|
32
32
|
export {
|
|
33
33
|
CodePushFailed,
|
|
34
34
|
ConvexAppNotFound,
|
|
@@ -9,17 +9,18 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type * as state from "../state.js";
|
|
12
|
+
import type * as version from "../version.js";
|
|
12
13
|
|
|
13
14
|
import type {
|
|
14
15
|
ApiFromModules,
|
|
15
16
|
FilterApi,
|
|
16
17
|
FunctionReference,
|
|
17
18
|
} from "convex/server";
|
|
18
|
-
import { anyApi, componentsGeneric } from "convex/server";
|
|
19
19
|
|
|
20
|
-
const fullApi: ApiFromModules<{
|
|
20
|
+
declare const fullApi: ApiFromModules<{
|
|
21
21
|
state: typeof state;
|
|
22
|
-
|
|
22
|
+
version: typeof version;
|
|
23
|
+
}>;
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* A utility for referencing Convex functions in your app's public API.
|
|
@@ -29,10 +30,10 @@ const fullApi: ApiFromModules<{
|
|
|
29
30
|
* const myFunctionReference = api.myModule.myFunction;
|
|
30
31
|
* ```
|
|
31
32
|
*/
|
|
32
|
-
export const api: FilterApi<
|
|
33
|
+
export declare const api: FilterApi<
|
|
33
34
|
typeof fullApi,
|
|
34
35
|
FunctionReference<any, "public">
|
|
35
|
-
|
|
36
|
+
>;
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* A utility for referencing Convex functions in your app's internal API.
|
|
@@ -42,9 +43,9 @@ export const api: FilterApi<
|
|
|
42
43
|
* const myFunctionReference = internal.myModule.myFunction;
|
|
43
44
|
* ```
|
|
44
45
|
*/
|
|
45
|
-
export const internal: FilterApi<
|
|
46
|
+
export declare const internal: FilterApi<
|
|
46
47
|
typeof fullApi,
|
|
47
48
|
FunctionReference<any, "internal">
|
|
48
|
-
|
|
49
|
+
>;
|
|
49
50
|
|
|
50
|
-
export const components
|
|
51
|
+
export declare const components: {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/**
|
|
3
|
+
* Generated `api` utility.
|
|
4
|
+
*
|
|
5
|
+
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
6
|
+
*
|
|
7
|
+
* To regenerate, run `npx convex dev`.
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { anyApi, componentsGeneric } from "convex/server";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A utility for referencing Convex functions in your app's API.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* ```js
|
|
18
|
+
* const myFunctionReference = api.myModule.myFunction;
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const api = anyApi;
|
|
22
|
+
export const internal = anyApi;
|
|
23
|
+
export const components = componentsGeneric();
|