@convex-dev/workpool 0.2.0-beta.0 → 0.2.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.
Files changed (110) hide show
  1. package/README.md +87 -18
  2. package/dist/commonjs/client/index.d.ts +33 -8
  3. package/dist/commonjs/client/index.d.ts.map +1 -1
  4. package/dist/commonjs/client/index.js +37 -7
  5. package/dist/commonjs/client/index.js.map +1 -1
  6. package/dist/commonjs/component/complete.d.ts +89 -0
  7. package/dist/commonjs/component/complete.d.ts.map +1 -0
  8. package/dist/commonjs/component/complete.js +82 -0
  9. package/dist/commonjs/component/complete.js.map +1 -0
  10. package/dist/commonjs/component/kick.d.ts +3 -3
  11. package/dist/commonjs/component/kick.d.ts.map +1 -1
  12. package/dist/commonjs/component/kick.js +17 -12
  13. package/dist/commonjs/component/kick.js.map +1 -1
  14. package/dist/commonjs/component/lib.d.ts +6 -6
  15. package/dist/commonjs/component/lib.d.ts.map +1 -1
  16. package/dist/commonjs/component/lib.js +53 -24
  17. package/dist/commonjs/component/lib.js.map +1 -1
  18. package/dist/commonjs/component/logging.d.ts +3 -2
  19. package/dist/commonjs/component/logging.d.ts.map +1 -1
  20. package/dist/commonjs/component/logging.js +34 -16
  21. package/dist/commonjs/component/logging.js.map +1 -1
  22. package/dist/commonjs/component/loop.d.ts +1 -14
  23. package/dist/commonjs/component/loop.d.ts.map +1 -1
  24. package/dist/commonjs/component/loop.js +216 -179
  25. package/dist/commonjs/component/loop.js.map +1 -1
  26. package/dist/commonjs/component/recovery.d.ts +45 -0
  27. package/dist/commonjs/component/recovery.d.ts.map +1 -1
  28. package/dist/commonjs/component/recovery.js +88 -65
  29. package/dist/commonjs/component/recovery.js.map +1 -1
  30. package/dist/commonjs/component/schema.d.ts +17 -13
  31. package/dist/commonjs/component/schema.d.ts.map +1 -1
  32. package/dist/commonjs/component/schema.js +5 -3
  33. package/dist/commonjs/component/schema.js.map +1 -1
  34. package/dist/commonjs/component/shared.d.ts +24 -15
  35. package/dist/commonjs/component/shared.d.ts.map +1 -1
  36. package/dist/commonjs/component/shared.js +20 -7
  37. package/dist/commonjs/component/shared.js.map +1 -1
  38. package/dist/commonjs/component/stats.d.ts +36 -29
  39. package/dist/commonjs/component/stats.d.ts.map +1 -1
  40. package/dist/commonjs/component/stats.js +110 -52
  41. package/dist/commonjs/component/stats.js.map +1 -1
  42. package/dist/commonjs/component/worker.d.ts +4 -14
  43. package/dist/commonjs/component/worker.d.ts.map +1 -1
  44. package/dist/commonjs/component/worker.js +23 -36
  45. package/dist/commonjs/component/worker.js.map +1 -1
  46. package/dist/esm/client/index.d.ts +33 -8
  47. package/dist/esm/client/index.d.ts.map +1 -1
  48. package/dist/esm/client/index.js +37 -7
  49. package/dist/esm/client/index.js.map +1 -1
  50. package/dist/esm/component/complete.d.ts +89 -0
  51. package/dist/esm/component/complete.d.ts.map +1 -0
  52. package/dist/esm/component/complete.js +82 -0
  53. package/dist/esm/component/complete.js.map +1 -0
  54. package/dist/esm/component/kick.d.ts +3 -3
  55. package/dist/esm/component/kick.d.ts.map +1 -1
  56. package/dist/esm/component/kick.js +17 -12
  57. package/dist/esm/component/kick.js.map +1 -1
  58. package/dist/esm/component/lib.d.ts +6 -6
  59. package/dist/esm/component/lib.d.ts.map +1 -1
  60. package/dist/esm/component/lib.js +53 -24
  61. package/dist/esm/component/lib.js.map +1 -1
  62. package/dist/esm/component/logging.d.ts +3 -2
  63. package/dist/esm/component/logging.d.ts.map +1 -1
  64. package/dist/esm/component/logging.js +34 -16
  65. package/dist/esm/component/logging.js.map +1 -1
  66. package/dist/esm/component/loop.d.ts +1 -14
  67. package/dist/esm/component/loop.d.ts.map +1 -1
  68. package/dist/esm/component/loop.js +216 -179
  69. package/dist/esm/component/loop.js.map +1 -1
  70. package/dist/esm/component/recovery.d.ts +45 -0
  71. package/dist/esm/component/recovery.d.ts.map +1 -1
  72. package/dist/esm/component/recovery.js +88 -65
  73. package/dist/esm/component/recovery.js.map +1 -1
  74. package/dist/esm/component/schema.d.ts +17 -13
  75. package/dist/esm/component/schema.d.ts.map +1 -1
  76. package/dist/esm/component/schema.js +5 -3
  77. package/dist/esm/component/schema.js.map +1 -1
  78. package/dist/esm/component/shared.d.ts +24 -15
  79. package/dist/esm/component/shared.d.ts.map +1 -1
  80. package/dist/esm/component/shared.js +20 -7
  81. package/dist/esm/component/shared.js.map +1 -1
  82. package/dist/esm/component/stats.d.ts +36 -29
  83. package/dist/esm/component/stats.d.ts.map +1 -1
  84. package/dist/esm/component/stats.js +110 -52
  85. package/dist/esm/component/stats.js.map +1 -1
  86. package/dist/esm/component/worker.d.ts +4 -14
  87. package/dist/esm/component/worker.d.ts.map +1 -1
  88. package/dist/esm/component/worker.js +23 -36
  89. package/dist/esm/component/worker.js.map +1 -1
  90. package/package.json +12 -12
  91. package/src/client/index.ts +82 -43
  92. package/src/component/README.md +15 -15
  93. package/src/component/_generated/api.d.ts +10 -5
  94. package/src/component/complete.test.ts +508 -0
  95. package/src/component/complete.ts +109 -0
  96. package/src/component/kick.test.ts +29 -19
  97. package/src/component/kick.ts +25 -17
  98. package/src/component/lib.test.ts +262 -17
  99. package/src/component/lib.ts +68 -30
  100. package/src/component/logging.test.ts +16 -0
  101. package/src/component/logging.ts +45 -24
  102. package/src/component/loop.test.ts +1158 -0
  103. package/src/component/loop.ts +292 -224
  104. package/src/component/recovery.test.ts +536 -0
  105. package/src/component/recovery.ts +100 -75
  106. package/src/component/schema.ts +6 -4
  107. package/src/component/shared.ts +23 -8
  108. package/src/component/stats.test.ts +345 -0
  109. package/src/component/stats.ts +149 -56
  110. package/src/component/worker.ts +25 -38
@@ -1,18 +1,20 @@
1
1
  import { v } from "convex/values";
2
- import { mutation, query } from "./_generated/server.js";
2
+ import { api } from "./_generated/api.js";
3
+ import { Id } from "./_generated/dataModel.js";
4
+ import { mutation, MutationCtx, query } from "./_generated/server.js";
5
+ import { kickMainLoop } from "./kick.js";
6
+ import { createLogger, LogLevel, logLevel } from "./logging.js";
3
7
  import {
4
- nextSegment,
8
+ boundScheduledTime,
9
+ config,
10
+ getNextSegment,
11
+ max,
5
12
  onComplete,
6
13
  retryBehavior,
7
- config,
8
14
  status as statusValidator,
9
15
  toSegment,
10
- boundScheduledTime,
11
16
  } from "./shared.js";
12
- import { logLevel } from "./logging.js";
13
- import { kickMainLoop } from "./kick.js";
14
- import { api } from "./_generated/api.js";
15
- import { createLogger } from "./logging.js";
17
+ import { recordEnqueued } from "./stats.js";
16
18
 
17
19
  const MAX_POSSIBLE_PARALLELISM = 100;
18
20
 
@@ -42,12 +44,12 @@ export const enqueue = mutation({
42
44
  ...workArgs,
43
45
  attempts: 0,
44
46
  });
47
+ const limit = await kickMainLoop(ctx, "enqueue", config);
45
48
  await ctx.db.insert("pendingStart", {
46
49
  workId,
47
- segment: toSegment(runAt),
50
+ segment: max(toSegment(runAt), limit),
48
51
  });
49
- await kickMainLoop(ctx, "enqueue", config);
50
- // TODO: stats event
52
+ recordEnqueued(console, { workId, fnName: workArgs.fnName, runAt });
51
53
  return workId;
52
54
  },
53
55
  });
@@ -58,12 +60,14 @@ export const cancel = mutation({
58
60
  logLevel,
59
61
  },
60
62
  handler: async (ctx, { id, logLevel }) => {
61
- await ctx.db.insert("pendingCancelation", {
62
- workId: id,
63
- segment: nextSegment(),
64
- });
65
- await kickMainLoop(ctx, "cancel", { logLevel });
66
- // TODO: stats event
63
+ const shouldCancel = await shouldCancelWorkItem(ctx, id, logLevel);
64
+ if (shouldCancel) {
65
+ const segment = await kickMainLoop(ctx, "cancel", { logLevel });
66
+ await ctx.db.insert("pendingCancelation", {
67
+ workId: id,
68
+ segment,
69
+ });
70
+ }
67
71
  },
68
72
  });
69
73
 
@@ -72,23 +76,28 @@ export const cancelAll = mutation({
72
76
  args: { logLevel, before: v.optional(v.number()) },
73
77
  handler: async (ctx, { logLevel, before }) => {
74
78
  const beforeTime = before ?? Date.now();
75
- const segment = nextSegment();
76
79
  const pageOfWork = await ctx.db
77
80
  .query("work")
78
81
  .withIndex("by_creation_time", (q) => q.lte("_creationTime", beforeTime))
79
82
  .order("desc")
80
83
  .take(PAGE_SIZE);
84
+ const shouldCancel = await Promise.all(
85
+ pageOfWork.map(async ({ _id }) =>
86
+ shouldCancelWorkItem(ctx, _id, logLevel)
87
+ )
88
+ );
89
+ let segment = getNextSegment();
90
+ if (shouldCancel.some((c) => c)) {
91
+ segment = await kickMainLoop(ctx, "cancel", { logLevel });
92
+ }
81
93
  await Promise.all(
82
- pageOfWork.map(async ({ _id }) => {
83
- if (
84
- await ctx.db
85
- .query("pendingCancelation")
86
- .withIndex("workId", (q) => q.eq("workId", _id))
87
- .first()
88
- ) {
89
- return;
94
+ pageOfWork.map(({ _id }, index) => {
95
+ if (shouldCancel[index]) {
96
+ return ctx.db.insert("pendingCancelation", {
97
+ workId: _id,
98
+ segment,
99
+ });
90
100
  }
91
- await ctx.db.insert("pendingCancelation", { workId: _id, segment });
92
101
  })
93
102
  );
94
103
  if (pageOfWork.length === PAGE_SIZE) {
@@ -97,7 +106,6 @@ export const cancelAll = mutation({
97
106
  before: pageOfWork[pageOfWork.length - 1]._creationTime,
98
107
  });
99
108
  }
100
- await kickMainLoop(ctx, "cancel", { logLevel });
101
109
  },
102
110
  });
103
111
 
@@ -114,12 +122,42 @@ export const status = query({
114
122
  .withIndex("workId", (q) => q.eq("workId", id))
115
123
  .unique();
116
124
  if (pendingStart) {
117
- return { state: "pending", attempt: work.attempts } as const;
125
+ return { state: "pending", previousAttempts: work.attempts } as const;
126
+ }
127
+ const pendingCompletion = await ctx.db
128
+ .query("pendingCompletion")
129
+ .withIndex("workId", (q) => q.eq("workId", id))
130
+ .unique();
131
+ if (pendingCompletion?.retry) {
132
+ return { state: "pending", previousAttempts: work.attempts } as const;
118
133
  }
119
134
  // Assume it's in progress. It could be pending cancelation
120
- return { state: "running", attempt: work.attempts } as const;
135
+ return { state: "running", previousAttempts: work.attempts } as const;
121
136
  },
122
137
  });
123
138
 
139
+ async function shouldCancelWorkItem(
140
+ ctx: MutationCtx,
141
+ workId: Id<"work">,
142
+ logLevel: LogLevel
143
+ ) {
144
+ const console = createLogger(logLevel);
145
+ // No-op if the work doesn't exist or has completed.
146
+ const work = await ctx.db.get(workId);
147
+ if (!work) {
148
+ console.warn(`[cancel] work ${workId} doesn't exist`);
149
+ return false;
150
+ }
151
+ const pendingCancelation = await ctx.db
152
+ .query("pendingCancelation")
153
+ .withIndex("workId", (q) => q.eq("workId", workId))
154
+ .unique();
155
+ if (pendingCancelation) {
156
+ console.warn(`[cancel] work ${workId} has already been canceled`);
157
+ return false;
158
+ }
159
+ return true;
160
+ }
161
+
124
162
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
125
163
  const console = "THIS IS A REMINDER TO USE createLogger";
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { shouldLog } from "./logging";
3
+
4
+ describe("logging", () => {
5
+ describe("shouldLog", () => {
6
+ it("should return true if the log level is above the config level", () => {
7
+ expect(shouldLog("INFO", "DEBUG")).toBe(false);
8
+ });
9
+ it("should return false if the log level is below the config level", () => {
10
+ expect(shouldLog("INFO", "WARN")).toBe(true);
11
+ });
12
+ it("should return true if the log level is equal to the config level", () => {
13
+ expect(shouldLog("INFO", "INFO")).toBe(true);
14
+ });
15
+ });
16
+ });
@@ -1,6 +1,17 @@
1
1
  import { v, Infer } from "convex/values";
2
2
 
3
- export const DEFAULT_LOG_LEVEL: LogLevel = "WARN";
3
+ export const DEFAULT_LOG_LEVEL: LogLevel = "REPORT";
4
+
5
+ // NOTE: the ordering here is important! A config level of "INFO" will log
6
+ // "INFO", "REPORT", "WARN",and "ERROR" events.
7
+ export const logLevel = v.union(
8
+ v.literal("DEBUG"),
9
+ v.literal("INFO"),
10
+ v.literal("REPORT"),
11
+ v.literal("WARN"),
12
+ v.literal("ERROR")
13
+ );
14
+ export type LogLevel = Infer<typeof logLevel>;
4
15
 
5
16
  export type Logger = {
6
17
  debug: (...args: unknown[]) => void;
@@ -12,60 +23,70 @@ export type Logger = {
12
23
  event: (event: string, payload: Record<string, unknown>) => void;
13
24
  };
14
25
 
26
+ const logLevelOrder = logLevel.members.map((l) => l.value);
27
+ const logLevelByName = logLevelOrder.reduce(
28
+ (acc, l, i) => {
29
+ acc[l] = i;
30
+ return acc;
31
+ },
32
+ {} as Record<LogLevel, number>
33
+ );
34
+ export function shouldLog(config: LogLevel, level: LogLevel) {
35
+ return logLevelByName[config] <= logLevelByName[level];
36
+ }
37
+ const DEBUG = logLevelByName["DEBUG"];
38
+ const INFO = logLevelByName["INFO"];
39
+ const REPORT = logLevelByName["REPORT"];
40
+ const WARN = logLevelByName["WARN"];
41
+ const ERROR = logLevelByName["ERROR"];
42
+
15
43
  export function createLogger(level?: LogLevel): Logger {
16
- const levelIndex = ["DEBUG", "INFO", "WARN", "ERROR"].indexOf(
17
- level ?? DEFAULT_LOG_LEVEL
18
- );
19
- if (levelIndex === -1) {
44
+ const levelIndex = logLevelByName[level ?? DEFAULT_LOG_LEVEL];
45
+ if (levelIndex === undefined) {
20
46
  throw new Error(`Invalid log level: ${level}`);
21
47
  }
22
48
  return {
23
49
  debug: (...args: unknown[]) => {
24
- if (levelIndex <= 0) {
50
+ if (levelIndex <= DEBUG) {
25
51
  console.debug(...args);
26
52
  }
27
53
  },
28
54
  info: (...args: unknown[]) => {
29
- if (levelIndex <= 1) {
55
+ if (levelIndex <= INFO) {
30
56
  console.info(...args);
31
57
  }
32
58
  },
33
59
  warn: (...args: unknown[]) => {
34
- if (levelIndex <= 2) {
60
+ if (levelIndex <= WARN) {
35
61
  console.warn(...args);
36
62
  }
37
63
  },
38
64
  error: (...args: unknown[]) => {
39
- if (levelIndex <= 3) {
65
+ if (levelIndex <= ERROR) {
40
66
  console.error(...args);
41
67
  }
42
68
  },
43
69
  time: (label: string) => {
44
- if (levelIndex <= 0) {
70
+ if (levelIndex <= DEBUG) {
45
71
  console.time(label);
46
72
  }
47
73
  },
48
74
  timeEnd: (label: string) => {
49
- if (levelIndex <= 0) {
75
+ if (levelIndex <= DEBUG) {
50
76
  console.timeEnd(label);
51
77
  }
52
78
  },
53
79
  event: (event: string, payload: Record<string, unknown>) => {
54
- if (levelIndex <= 1) {
55
- const fullPayload = {
56
- system: "idempotent-workpool-component",
57
- event,
58
- payload,
59
- };
80
+ const fullPayload = {
81
+ component: "workpool",
82
+ event,
83
+ ...payload,
84
+ };
85
+ if (levelIndex === REPORT && event === "report") {
86
+ console.info(JSON.stringify(fullPayload));
87
+ } else if (levelIndex <= INFO) {
60
88
  console.info(JSON.stringify(fullPayload));
61
89
  }
62
90
  },
63
91
  };
64
92
  }
65
- export const logLevel = v.union(
66
- v.literal("DEBUG"),
67
- v.literal("INFO"),
68
- v.literal("WARN"),
69
- v.literal("ERROR")
70
- );
71
- export type LogLevel = Infer<typeof logLevel>;