@samebase/alchemy-convex 0.5.0 → 0.6.1

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/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
- // The state of every stage lives in the alchemyState component
4
- // (`@samebase/alchemy-convex/convex.config`), which the app mounts and pushes
5
- // to the production deployment of its project. The store calls the component
6
- // functions through POST <deployment>/api/function with a component path, the
7
- // call that `convex run --component` makes, with the deploy key in
8
- // CONVEX_STATE_DEPLOY_KEY. Measured: a component query needs the key action
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: Alchemy reads state before the first push, when the deployment
13
- // has no alchemyState component yet. The store asks the component list of the
14
- // deployment, and only a deployment without the component means empty state.
15
- // The store then keeps the state of the run in memory and moves it to the
16
- // component on the first write after the component is active: the first
17
- // write after the push. A run that ends with its state still in memory fails
18
- // at the stack output. An unmounted component keeps its rows, so it fails.
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
- // The function names of the component are a wire contract. An older
21
- // component must answer a newer store, so a name is never renamed or removed.
22
- import * as Config from "effect/Config";
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 { keyTargetOf } from "./Push.ts";
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
- /** Environment variable with the deploy key of the deployment that holds the state. */
42
- export const STATE_DEPLOY_KEY_ENV = "CONVEX_STATE_DEPLOY_KEY";
75
+ /** The name of the state project in each team. */
76
+ export const STORE_PROJECT_NAME = "alchemy-state";
43
77
 
44
- /** The component path of alchemyState in the app: its default name. */
45
- const COMPONENT_PATH = "alchemyState";
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 and /api/query, with HTTP 200 or 560 for a function error. */
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 value of `_system/frontend/components:list`: the root has path "", a removed mount is "unmounted". */
54
- const Components = Schema.Array(Schema.Struct({ path: Schema.String, state: Schema.String }));
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
- /** Runs a function of the component, or a system function of the root with `componentPath` undefined. */
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
- componentPath?: string,
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, componentPath) =>
158
+ call: (path, args) =>
125
159
  post(
126
- // A component function answers only through /api/function. A system query
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
- { ...(componentPath === undefined ? {} : { componentPath }), path, args, format: "json" },
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 state of the alchemyState mount: "active", "unmounted" (its rows are kept), or undefined. */
144
- const mountOf = (api: FunctionApi) =>
145
- api.call("_system/frontend/components:list", {}).pipe(
146
- Effect.flatMap(decode(Components, "component list")),
147
- Effect.map((components) => components.find((c) => c.path === COMPONENT_PATH)?.state),
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 component functions with Alchemy's encoding: the JSON text of encodeState. */
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, COMPONENT_PATH);
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,227 @@ const remoteOf = (api: FunctionApi) => {
189
211
  };
190
212
  };
191
213
 
192
- const unmounted = failure(
193
- `the ${COMPONENT_PATH} component of the deployment is unmounted. Its rows are kept: mount @samebase/alchemy-convex/convex.config in convex/convex.config.ts again.`,
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, or undefined with a team access token, which names its team. */
237
+ readonly team: string | number | undefined;
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
- * The StateService over `api`. Without an alchemyState component at
198
- * the start, the state of the run stays in memory until a write finds the
199
- * component active.
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
- export const makeConvexState = (api: FunctionApi) =>
251
+ const pushStore = (
252
+ options: StoreOptions,
253
+ deployment: { readonly name: string; readonly deploymentUrl: string },
254
+ ) =>
202
255
  Effect.gen(function* () {
203
- const remote = remoteOf(api);
204
- const mount = yield* mountOf(api);
205
- if (mount === "unmounted") return yield* unmounted;
206
- let pending = mount === undefined;
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
- // The records that InMemoryService writes to, read again by the flush.
209
- const rows: Record<string, Record<string, Record<string, PersistedState>>> = {};
210
- const outputs: Record<string, Record<string, unknown>> = {};
211
- const memory = yield* InMemoryService(
212
- // @ts-expect-error InMemoryService types the records as ResourceState, and its set stores every PersistedState.
213
- rows,
214
- outputs,
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
- /** Moves the memory to the component once it is active. Rows that the run did not read stop it. */
218
- const flush = Effect.gen(function* () {
219
- const now = yield* mountOf(api);
220
- if (now === undefined) return;
221
- if (now !== "active") return yield* unmounted;
222
- const stages = [...new Set([...Object.keys(rows), ...Object.keys(outputs)])].flatMap(
223
- (stack) =>
224
- [
225
- ...new Set([...Object.keys(rows[stack] ?? {}), ...Object.keys(outputs[stack] ?? {})]),
226
- ].map((stage) => ({ stack, stage })),
227
- );
228
- for (const { stack, stage } of stages) {
229
- const found = yield* remote.list({ stack, stage });
230
- if (found.length > 0) {
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 } = options;
317
+ const teamId = yield* api.resolveTeamId(options.team);
318
+ const team = options.team ?? teamId;
319
+ const found = yield* findProjectByName(api, teamId, STORE_PROJECT_NAME);
320
+ const project = found ?? (yield* createStoreProject(api, teamId, team));
321
+ const deployment = yield* prodDeploymentOf(api, project.id);
322
+ if (deployment === undefined) {
323
+ return yield* new ProductionDeploymentNotFound({ projectId: project.id });
324
+ }
325
+
326
+ // The keys of this run, deleted together when the scope closes. A call
327
+ // that the current key no longer authorizes gets one new key and one
328
+ // more attempt.
329
+ const keys: Redacted.Redacted<string>[] = [];
330
+ yield* Scope.addFinalizer(
331
+ options.scope,
332
+ Effect.suspend(() =>
333
+ Effect.forEach(keys, (key) => deleteDeployKey(api, deployment.name, key), {
334
+ discard: true,
335
+ }),
336
+ ),
337
+ );
338
+ const mint = createDeployKey(api, deployment.name, "alchemy-state", RUN_KEY_ACTIONS).pipe(
339
+ Effect.map((key) => {
340
+ keys.push(key);
341
+ return makeFunctionApi(deployment.deploymentUrl, key);
342
+ }),
343
+ Effect.mapError((error) => failure(`no key for ${deployment.name}: ${error.message}`)),
344
+ );
345
+ let current = yield* mint;
346
+ const functions: FunctionApi = {
347
+ call: (path, args) =>
348
+ current.call(path, args).pipe(
349
+ Effect.catchIf(isUnauthorized, () =>
350
+ mint.pipe(
351
+ Effect.flatMap((next) => {
352
+ current = next;
353
+ return next.call(path, args);
354
+ }),
355
+ ),
356
+ ),
357
+ ),
358
+ };
359
+
360
+ const version = yield* storeVersion(functions);
361
+ if (version === undefined || version < STORE_VERSION) {
362
+ // Code without `state:version` is another app with the name of the store.
363
+ if (version === undefined && deployment.lastDeployTime) {
364
+ return yield* failure(
365
+ `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.`,
366
+ );
367
+ }
368
+ // Another run can push a newer store in the meantime: read again right
369
+ // before the push. The push itself has no lock (see the design note).
370
+ const now = yield* storeVersion(functions);
371
+ if (now === undefined || now < STORE_VERSION) {
372
+ yield* pushStore(options, deployment);
373
+ const pushed = yield* storeVersion(functions);
374
+ if (pushed === undefined || pushed < STORE_VERSION) {
231
375
  return yield* failure(
232
- `${stack}/${stage} has ${found.length} rows in the deployment that this run did not read, so the run wrote none. Run it again.`,
376
+ `${deployment.name} answers store version ${pushed ?? "none"} after the push of version ${STORE_VERSION}.`,
233
377
  );
234
378
  }
235
379
  }
236
- for (const { stack, stage } of stages) {
237
- for (const [fqn, value] of Object.entries(rows[stack]?.[stage] ?? {})) {
238
- yield* remote.set({ stack, stage, fqn, value });
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),
380
+ } else if (version > STORE_VERSION) {
381
+ // The names are a wire contract, so a newer store serves this client.
382
+ yield* Effect.logInfo(
383
+ `Convex state: the store of team ${team} is version ${version}, newer than version ${STORE_VERSION} of this package, and it stays.`,
259
384
  );
385
+ }
260
386
 
387
+ const remote = remoteOf(functions);
261
388
  const service: StateService = {
262
389
  id: "convex",
263
390
  getVersion: () => Effect.succeed(STATE_STORE_VERSION),
264
- listStacks: () => read(memory.listStacks(), remote.listStacks()),
265
- listStages: (stack) => read(memory.listStages(stack), remote.listStages(stack)),
266
- list: (request) => read(memory.list(request), remote.list(request)),
267
- get: (request) => read(memory.get(request), remote.get(request)),
268
- getReplacedResources: (request) =>
269
- read(memory.getReplacedResources(request), remote.getReplacedResources(request)),
270
- getOutput: (request) => read(memory.getOutput(request), remote.getOutput(request)),
271
- set: (request) => write(memory.set(request), remote.set(request), request.value),
272
- delete: (request) => write(memory.delete(request), remote.delete(request), undefined),
273
- deleteStack: (request) =>
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
- ),
391
+ listStacks: remote.listStacks,
392
+ listStages: remote.listStages,
393
+ list: remote.list,
394
+ get: remote.get,
395
+ getReplacedResources: remote.getReplacedResources,
396
+ getOutput: remote.getOutput,
397
+ set: (request) => remote.set(request).pipe(Effect.as(request.value)),
398
+ delete: (request) => remote.delete(request).pipe(Effect.asVoid),
399
+ deleteStack: (request) => remote.deleteStack(request).pipe(Effect.asVoid),
400
+ setOutput: (request) => remote.setOutput(request).pipe(Effect.as(request.value)),
288
401
  };
289
402
  return service;
290
403
  });
291
404
 
292
405
  /**
293
- * Alchemy state in the alchemyState component of the deployment that the
294
- * deploy key in CONVEX_STATE_DEPLOY_KEY names: the production deployment of
295
- * the project. The key needs the actions `deployment:data:view` and
296
- * `deployment:data:write`.
406
+ * Alchemy state in the `alchemy-state` project of `team`, with the team token
407
+ * in CONVEX_ACCESS_TOKEN or the Convex CLI login. A team access token names
408
+ * its team, so `team` is optional with one; the Convex CLI login needs the
409
+ * slug or the id. The first use of the run finds or creates the project and
410
+ * pushes the store when it is missing or older. `alchemy plan` on another
411
+ * stack never touches Convex.
297
412
  */
298
- export const state = (): Layer.Layer<State> =>
413
+ export const state = (
414
+ options: { readonly team?: string | number } = {},
415
+ ): Layer.Layer<State, never, ChildProcessSpawner.ChildProcessSpawner> =>
299
416
  Layer.effect(
300
417
  State,
301
418
  Effect.gen(function* () {
302
- // The stack's ConfigProvider, for the read of the key on first use.
419
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
420
+ const scope = yield* Scope.Scope;
421
+ // The stack's ConfigProvider, for the read of the token on first use.
303
422
  const context = yield* Effect.context<never>();
304
423
  return yield* Effect.cached(
305
424
  Effect.gen(function* () {
306
- const key = yield* Config.Redacted(STATE_DEPLOY_KEY_ENV);
307
- if (keyTargetOf(Redacted.value(key)).kind !== "deployment") {
308
- return yield* failure(
309
- `${STATE_DEPLOY_KEY_ENV} must be the deploy key of one deployment, such as prod:<name>|..., not a preview deploy key.`,
310
- );
311
- }
312
- return yield* makeConvexState(makeFunctionApi(yield* urlForKey(key), key));
425
+ const api = yield* ManagementApi.pipe(
426
+ Effect.provide(ManagementApiLive().pipe(Layer.provide(fromConfig()))),
427
+ );
428
+ return yield* makeConvexState({
429
+ team: options.team,
430
+ api,
431
+ spawner,
432
+ scope,
433
+ store: storeDirectory(),
434
+ });
313
435
  }).pipe(Effect.provideContext(context), Effect.orDie),
314
436
  );
315
437
  }),
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 { STATE_DEPLOY_KEY_ENV, state } from "./State.ts";
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
- }> = anyApi as any;
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
- > = anyApi as any;
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
- > = anyApi as any;
49
+ >;
49
50
 
50
- export const components = componentsGeneric() as unknown as {};
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();