@samebase/alchemy-convex 0.25.0 → 0.25.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samebase/alchemy-convex",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "description": "The Convex plane for Alchemy v2: projects, deployments, environment variables, and the push of the functions",
5
5
  "keywords": [
6
6
  "alchemy",
@@ -12,6 +12,21 @@ export const runRecord = v.object({
12
12
  recordedAt: v.string(),
13
13
  });
14
14
 
15
+ type RunRecord = Infer<typeof runRecord>;
16
+
17
+ /**
18
+ * True when a write with `next` must replace `current`, the record of the stage. Records that differ
19
+ * only in `recordedAt`, the time of one write, name the same run. Two laptop deploys of one commit
20
+ * have no `runUrl`, so they are the same run too. A write without a record (`null`) clears one.
21
+ */
22
+ export const runChanged = (current: RunRecord | undefined, next: RunRecord | null) => {
23
+ if (next === null || current === undefined) return next !== null || current !== undefined;
24
+ const other = new Map(Object.entries(next));
25
+ return Object.entries(current).some(
26
+ ([field, value]) => field !== "recordedAt" && other.get(field) !== value,
27
+ );
28
+ };
29
+
15
30
  /** Whether `value` passes `validator`. Only the kinds of {@link runRecord} pass; another kind fails. */
16
31
  export const validate = <V extends GenericValidator>(
17
32
  validator: V,
@@ -10,7 +10,7 @@ import type { MutationCtx, QueryCtx } from "./_generated/server.js";
10
10
  // oxlint-disable-next-line samebase/no-raw-public-convex-function-builders -- Store entrypoints have no app wrapper; the reader and the deploy key enforce access.
11
11
  import { mutation, query } from "./_generated/server.js";
12
12
  import { generationOf } from "./keys.ts";
13
- import { runRecord } from "./runRecord.ts";
13
+ import { runChanged, runRecord } from "./runRecord.ts";
14
14
 
15
15
  const stage = { stack: v.string(), stage: v.string() };
16
16
  const resource = { ...stage, fqn: v.string() };
@@ -43,17 +43,31 @@ const outputOf = (ctx: QueryCtx, args: { stack: string; stage: string }) =>
43
43
  */
44
44
  const run = v.union(runRecord, v.null());
45
45
 
46
- /** A resource write and its current run become visible in one transaction. */
46
+ /**
47
+ * A resource write and its current run become visible in one transaction. The output row of the
48
+ * stage changes only when the run changes (see `runChanged`). Alchemy writes the resources of a
49
+ * deploy at the same time, and a write to the one output row of the stage made all of them conflict
50
+ * until Convex stopped the retries. A write of the same run only reads the row, and reads do not
51
+ * conflict. So `recordedAt` stays the time of the first write of the run, until `setOutput` writes
52
+ * it again.
53
+ */
47
54
  const writeCurrentRun = async (
48
55
  ctx: MutationCtx,
49
56
  args: { stack: string; stage: string; run: Infer<typeof run> },
50
57
  ) => {
51
58
  const output = await outputOf(ctx, args);
59
+ if (output === null) {
60
+ if (args.run !== null)
61
+ await ctx.db.insert("outputs", {
62
+ stack: args.stack,
63
+ stage: args.stage,
64
+ currentRun: args.run,
65
+ });
66
+ return;
67
+ }
52
68
  // A write without a run record clears the record of the previous run.
53
- if (output !== null)
69
+ if (runChanged(output.currentRun, args.run))
54
70
  await ctx.db.patch("outputs", output._id, { currentRun: args.run ?? undefined });
55
- else if (args.run !== null)
56
- await ctx.db.insert("outputs", { stack: args.stack, stage: args.stage, currentRun: args.run });
57
71
  };
58
72
 
59
73
  /** The sorted union of two lists of names. */
@@ -228,10 +242,19 @@ export const setOutput = mutation({
228
242
  returns: v.null(),
229
243
  handler: async (ctx, args) => {
230
244
  const row = await outputOf(ctx, args);
231
- if (row === null)
232
- await ctx.db.insert("outputs", { stack: args.stack, stage: args.stage, value: args.value });
233
- else await ctx.db.patch("outputs", row._id, { value: args.value });
234
- await writeCurrentRun(ctx, args);
245
+ // The output and the record of its run, with the time of this last write of the run.
246
+ if (row !== null)
247
+ await ctx.db.patch("outputs", row._id, {
248
+ value: args.value,
249
+ currentRun: args.run ?? undefined,
250
+ });
251
+ else
252
+ await ctx.db.insert("outputs", {
253
+ stack: args.stack,
254
+ stage: args.stage,
255
+ value: args.value,
256
+ ...(args.run === null ? {} : { currentRun: args.run }),
257
+ });
235
258
  return null;
236
259
  },
237
260
  });