ocel 0.0.1-alpha.0 → 0.0.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 (271) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +98 -0
  4. package/dist/binding/binding.d.ts +22 -0
  5. package/dist/binding/binding.js +60 -0
  6. package/dist/binding/unprovisioned.d.ts +17 -0
  7. package/dist/binding/unprovisioned.js +25 -0
  8. package/dist/bucket/bucket-client.d.ts +4 -0
  9. package/dist/bucket/bucket-client.js +5 -0
  10. package/dist/bucket/bucket-context.d.ts +8 -0
  11. package/dist/bucket/bucket-context.js +10 -0
  12. package/dist/bucket/bucket.d.ts +60 -0
  13. package/dist/bucket/bucket.js +91 -0
  14. package/dist/bucket/client.d.ts +49 -0
  15. package/dist/bucket/client.js +95 -0
  16. package/dist/bucket/errors.d.ts +12 -0
  17. package/dist/bucket/errors.js +20 -0
  18. package/dist/bucket/express.d.ts +10 -0
  19. package/dist/bucket/express.js +24 -0
  20. package/dist/bucket/hono.d.ts +18 -0
  21. package/dist/bucket/hono.js +13 -0
  22. package/dist/bucket/index.d.ts +8 -0
  23. package/dist/bucket/index.js +6 -0
  24. package/dist/bucket/keys.d.ts +5 -0
  25. package/dist/bucket/keys.js +24 -0
  26. package/dist/bucket/metadata.d.ts +6 -0
  27. package/dist/bucket/metadata.js +6 -0
  28. package/dist/bucket/next.d.ts +15 -0
  29. package/dist/bucket/next.js +11 -0
  30. package/dist/bucket/objects.d.ts +130 -0
  31. package/dist/bucket/objects.js +350 -0
  32. package/dist/bucket/route.d.ts +13 -0
  33. package/dist/bucket/route.js +247 -0
  34. package/dist/bucket/server-only.js +5 -0
  35. package/dist/bucket/types.d.ts +63 -0
  36. package/dist/bucket/uploader.d.ts +3 -0
  37. package/dist/bucket/uploader.js +6 -0
  38. package/dist/build-env.d.ts +17 -0
  39. package/dist/build-env.js +41 -0
  40. package/dist/config.d.ts +30 -0
  41. package/dist/config.js +9 -0
  42. package/dist/declaration/callsite.d.ts +15 -0
  43. package/dist/declaration/callsite.js +43 -0
  44. package/dist/declaration/component.d.ts +4 -0
  45. package/dist/declaration/component.js +1 -0
  46. package/dist/declaration/defer.d.ts +5 -0
  47. package/dist/declaration/defer.js +9 -0
  48. package/dist/delivery/duration.d.ts +13 -0
  49. package/dist/delivery/duration.js +36 -0
  50. package/dist/delivery/lane.d.ts +4 -0
  51. package/dist/delivery/lane.js +15 -0
  52. package/dist/delivery/payload.d.ts +6 -0
  53. package/dist/delivery/payload.js +14 -0
  54. package/dist/delivery/retry.d.ts +16 -0
  55. package/dist/delivery/retry.js +10 -0
  56. package/dist/delivery/schema.d.ts +10 -0
  57. package/dist/delivery/schema.js +20 -0
  58. package/dist/dns.d.ts +11 -0
  59. package/dist/dns.js +4 -0
  60. package/dist/edge.d.ts +4 -0
  61. package/dist/edge.js +4 -0
  62. package/dist/env/access.d.ts +22 -0
  63. package/dist/env/access.js +49 -0
  64. package/dist/env/client.d.ts +5 -0
  65. package/dist/env/client.js +10 -0
  66. package/dist/env/declare.d.ts +2 -0
  67. package/dist/env/declare.js +83 -0
  68. package/dist/env/definition.d.ts +46 -0
  69. package/dist/env/definition.js +164 -0
  70. package/dist/env/deployment.d.ts +23 -0
  71. package/dist/env/deployment.js +13 -0
  72. package/dist/env/edge.d.ts +10 -0
  73. package/dist/env/edge.js +30 -0
  74. package/dist/env/errors.d.ts +9 -0
  75. package/dist/env/errors.js +9 -0
  76. package/dist/env/file.d.ts +1 -0
  77. package/dist/env/file.js +27 -0
  78. package/dist/env/index.d.ts +10 -0
  79. package/dist/env/index.js +63 -0
  80. package/dist/env/live.d.ts +3 -0
  81. package/dist/env/live.js +12 -0
  82. package/dist/env/schema.d.ts +27 -0
  83. package/dist/env/schema.js +43 -0
  84. package/dist/env/scope.d.ts +7 -0
  85. package/dist/env/scope.js +45 -0
  86. package/dist/env/standard.d.ts +10 -0
  87. package/dist/env/standard.js +21 -0
  88. package/dist/env/value.d.ts +5 -0
  89. package/dist/env/value.js +26 -0
  90. package/dist/env-source.d.ts +17 -0
  91. package/dist/env-source.js +16 -0
  92. package/dist/gen/proto/app/bucket/v1/bucket_pb.d.ts +911 -0
  93. package/dist/gen/proto/app/bucket/v1/bucket_pb.js +253 -0
  94. package/dist/gen/proto/app/realtime/v1/realtime_pb.d.ts +50 -0
  95. package/dist/gen/proto/app/realtime/v1/realtime_pb.js +23 -0
  96. package/dist/gen/proto/app/resources/v1/resources_pb.d.ts +554 -0
  97. package/dist/gen/proto/app/resources/v1/resources_pb.js +214 -0
  98. package/dist/gen/proto/app/resources/v1/variables_pb.d.ts +231 -0
  99. package/dist/gen/proto/app/resources/v1/variables_pb.js +101 -0
  100. package/dist/gen/proto/app/task/v1/task_pb.d.ts +498 -0
  101. package/dist/gen/proto/app/task/v1/task_pb.js +151 -0
  102. package/dist/gen/proto/app/topic/v1/topic_pb.d.ts +456 -0
  103. package/dist/gen/proto/app/topic/v1/topic_pb.js +125 -0
  104. package/dist/gen/proto/buf/validate/validate_pb.d.ts +4833 -0
  105. package/dist/gen/proto/buf/validate/validate_pb.js +356 -0
  106. package/dist/gen/proto/common/bindings/v1/bindings_pb.d.ts +378 -0
  107. package/dist/gen/proto/common/bindings/v1/bindings_pb.js +140 -0
  108. package/dist/generated/config.d.ts +428 -0
  109. package/dist/generated/config.js +2 -0
  110. package/dist/kv/entries.d.ts +193 -0
  111. package/dist/kv/entries.js +325 -0
  112. package/dist/kv/errors.d.ts +12 -0
  113. package/dist/kv/errors.js +14 -0
  114. package/dist/kv/index.d.ts +6 -0
  115. package/dist/kv/index.js +3 -0
  116. package/dist/kv/ioredis.d.ts +2 -0
  117. package/dist/kv/ioredis.js +20 -0
  118. package/dist/kv/kv.d.ts +84 -0
  119. package/dist/kv/kv.js +177 -0
  120. package/dist/kv/pattern.d.ts +27 -0
  121. package/dist/kv/pattern.js +73 -0
  122. package/dist/kv/ttl.d.ts +22 -0
  123. package/dist/kv/ttl.js +21 -0
  124. package/dist/postgres/index.d.ts +21 -2
  125. package/dist/postgres/index.js +73 -2
  126. package/dist/postgres/pg.d.ts +5 -4
  127. package/dist/postgres/pg.js +16 -12
  128. package/dist/providers/aws/dns.d.ts +11 -0
  129. package/dist/providers/aws/dns.js +4 -0
  130. package/dist/providers/aws/edge.d.ts +6 -0
  131. package/dist/providers/aws/edge.js +8 -0
  132. package/dist/providers/aws/index.d.ts +4 -0
  133. package/dist/providers/aws/index.js +4 -0
  134. package/dist/providers/gcp/edge.d.ts +14 -0
  135. package/dist/providers/gcp/edge.js +14 -0
  136. package/dist/providers/gcp/index.d.ts +9 -0
  137. package/dist/providers/gcp/index.js +9 -0
  138. package/dist/providers/vps/index.d.ts +4 -0
  139. package/dist/providers/vps/index.js +4 -0
  140. package/dist/realtime/client-error.d.ts +30 -0
  141. package/dist/realtime/client-error.js +17 -0
  142. package/dist/realtime/client-event-socket.d.ts +8 -0
  143. package/dist/realtime/client-event-socket.js +169 -0
  144. package/dist/realtime/client-transport.d.ts +29 -0
  145. package/dist/realtime/client-transport.js +13 -0
  146. package/dist/realtime/client.d.ts +150 -0
  147. package/dist/realtime/client.js +345 -0
  148. package/dist/realtime/express.d.ts +18 -0
  149. package/dist/realtime/express.js +83 -0
  150. package/dist/realtime/handler.d.ts +56 -0
  151. package/dist/realtime/handler.js +287 -0
  152. package/dist/realtime/hono.d.ts +19 -0
  153. package/dist/realtime/hono.js +24 -0
  154. package/dist/realtime/index.d.ts +2 -0
  155. package/dist/realtime/index.js +2 -0
  156. package/dist/realtime/next.d.ts +26 -0
  157. package/dist/realtime/next.js +16 -0
  158. package/dist/realtime/realtime.d.ts +181 -0
  159. package/dist/realtime/realtime.js +205 -0
  160. package/dist/realtime/token.d.ts +20 -0
  161. package/dist/realtime/token.js +45 -0
  162. package/dist/realtime/transport.d.ts +11 -0
  163. package/dist/realtime/transport.js +15 -0
  164. package/dist/realtime/wire.d.ts +30 -0
  165. package/dist/realtime/wire.js +104 -0
  166. package/dist/runtime/dev-server.d.ts +8 -0
  167. package/dist/runtime/dev-server.js +8 -0
  168. package/dist/runtime/rpc.d.ts +23 -0
  169. package/dist/runtime/rpc.js +24 -0
  170. package/dist/runtime/transport.d.ts +11 -0
  171. package/dist/runtime/transport.js +35 -0
  172. package/dist/task/errors.d.ts +7 -0
  173. package/dist/task/errors.js +7 -0
  174. package/dist/task/hooks.d.ts +57 -0
  175. package/dist/task/hooks.js +1 -0
  176. package/dist/task/index.d.ts +9 -0
  177. package/dist/task/index.js +4 -0
  178. package/dist/task/runs.d.ts +74 -0
  179. package/dist/task/runs.js +82 -0
  180. package/dist/task/task.d.ts +117 -0
  181. package/dist/task/task.js +109 -0
  182. package/dist/topic/dead-letters.d.ts +53 -0
  183. package/dist/topic/dead-letters.js +62 -0
  184. package/dist/topic/index.d.ts +8 -0
  185. package/dist/topic/index.js +4 -0
  186. package/dist/topic/topic.d.ts +85 -0
  187. package/dist/topic/topic.js +117 -0
  188. package/dist/version.d.ts +2 -0
  189. package/dist/version.js +2 -0
  190. package/dist/worker/context.d.ts +36 -0
  191. package/dist/worker/context.js +1 -0
  192. package/dist/worker/deliver.d.ts +13 -0
  193. package/dist/worker/deliver.js +258 -0
  194. package/dist/worker/index.d.ts +4 -0
  195. package/dist/worker/index.js +2 -0
  196. package/dist/worker/registry.d.ts +28 -0
  197. package/dist/worker/registry.js +22 -0
  198. package/dist/worker/semaphore.d.ts +7 -0
  199. package/dist/worker/semaphore.js +36 -0
  200. package/dist/worker/worker.d.ts +21 -0
  201. package/dist/worker/worker.js +32 -0
  202. package/package.json +158 -57
  203. package/bin/run.js +0 -39
  204. package/dist/blob/bucket.d.ts +0 -55
  205. package/dist/blob/bucket.js +0 -65
  206. package/dist/blob/index.d.ts +0 -8
  207. package/dist/blob/index.js +0 -13
  208. package/dist/blob/integrations/client.d.ts +0 -25
  209. package/dist/blob/integrations/client.js +0 -71
  210. package/dist/blob/integrations/hono.d.ts +0 -31
  211. package/dist/blob/integrations/hono.js +0 -11
  212. package/dist/blob/mime-types.d.ts +0 -1
  213. package/dist/blob/parse.d.ts +0 -16
  214. package/dist/blob/parse.js +0 -69
  215. package/dist/blob/s3-listener.d.ts +0 -7
  216. package/dist/blob/s3-listener.js +0 -35
  217. package/dist/blob/types.d.ts +0 -128
  218. package/dist/blob/types.js +0 -1
  219. package/dist/blob/upload-error.d.ts +0 -35
  220. package/dist/blob/upload-error.js +0 -54
  221. package/dist/blob/uploader.d.ts +0 -67
  222. package/dist/blob/uploader.js +0 -323
  223. package/dist/internal/db/index.d.ts +0 -68
  224. package/dist/internal/db/index.js +0 -73
  225. package/dist/lambda/index.d.ts +0 -2
  226. package/dist/lambda/index.js +0 -4
  227. package/dist/lambda/integrations/express.d.ts +0 -3
  228. package/dist/lambda/integrations/express.js +0 -13
  229. package/dist/lambda/integrations/hono.d.ts +0 -3
  230. package/dist/lambda/integrations/hono.js +0 -13
  231. package/dist/lambda/lambda.d.ts +0 -43
  232. package/dist/lambda/lambda.js +0 -30
  233. package/dist/utils/component.d.ts +0 -3
  234. package/dist/utils/constants.d.ts +0 -2
  235. package/dist/utils/constants.js +0 -2
  236. package/dist/utils/nanoid.d.ts +0 -1
  237. package/dist/utils/nanoid.js +0 -6
  238. package/dist/utils/rpc.d.ts +0 -11
  239. package/dist/utils/rpc.js +0 -17
  240. package/dist/utils/stack.d.ts +0 -4
  241. package/dist/utils/stack.js +0 -23
  242. package/dist/workflow/index.d.ts +0 -3
  243. package/dist/workflow/index.js +0 -4
  244. package/dist/workflow/workflow.d.ts +0 -34
  245. package/dist/workflow/workflow.js +0 -146
  246. package/src/blob/bucket.ts +0 -92
  247. package/src/blob/index.ts +0 -34
  248. package/src/blob/integrations/client.ts +0 -127
  249. package/src/blob/integrations/hono.ts +0 -16
  250. package/src/blob/mime-types.ts +0 -68
  251. package/src/blob/parse.ts +0 -93
  252. package/src/blob/s3-listener.ts +0 -48
  253. package/src/blob/types.ts +0 -142
  254. package/src/blob/upload-error.ts +0 -67
  255. package/src/blob/uploader.ts +0 -426
  256. package/src/internal/db/index.ts +0 -78
  257. package/src/lambda/index.ts +0 -8
  258. package/src/lambda/integrations/express.ts +0 -25
  259. package/src/lambda/integrations/hono.ts +0 -25
  260. package/src/lambda/lambda.ts +0 -94
  261. package/src/postgres/index.ts +0 -5
  262. package/src/postgres/pg.ts +0 -33
  263. package/src/utils/component.ts +0 -3
  264. package/src/utils/constants.ts +0 -2
  265. package/src/utils/nanoid.ts +0 -9
  266. package/src/utils/rpc.ts +0 -28
  267. package/src/utils/stack.ts +0 -26
  268. package/src/workflow/index.ts +0 -9
  269. package/src/workflow/workflow.ts +0 -235
  270. /package/dist/{blob/mime-types.js → bucket/server-only.d.ts} +0 -0
  271. /package/dist/{utils/component.js → bucket/types.js} +0 -0
@@ -0,0 +1,117 @@
1
+ import { createClient } from "@connectrpc/connect";
2
+ import { refuseUnbound } from "../binding/binding.js";
3
+ import { unprovisioned, unprovisionedPhase } from "../binding/unprovisioned.js";
4
+ import { declarationSite } from "../declaration/callsite.js";
5
+ import { defer } from "../declaration/defer.js";
6
+ import { encodeDueAt, encodeDuration } from "../delivery/duration.js";
7
+ import { encodeLane } from "../delivery/lane.js";
8
+ import { encodePayload } from "../delivery/payload.js";
9
+ import { encodeRetryPolicy } from "../delivery/retry.js";
10
+ import { encodeJsonSchema } from "../delivery/schema.js";
11
+ import { ResourceType } from "../gen/proto/app/resources/v1/resources_pb.js";
12
+ import { TopicService } from "../gen/proto/app/topic/v1/topic_pb.js";
13
+ import { rpc } from "../runtime/rpc.js";
14
+ import { createRuntimeTransport } from "../runtime/transport.js";
15
+ import { DEFAULT_WORKER, registerRun } from "../worker/registry.js";
16
+ import { DeadLetters } from "./dead-letters.js";
17
+ /** A declared topic, and the handle its messages are sent through. */
18
+ export class Topic {
19
+ /** The name this topic was declared under. */
20
+ name;
21
+ schema;
22
+ client;
23
+ constructor(name, options) {
24
+ this.name = name;
25
+ this.schema = options.schema;
26
+ if (process.env.OCEL_PHASE === "discovery") {
27
+ defer(rpc.resource.declare({
28
+ resource: { name, type: ResourceType.TOPIC },
29
+ config: {
30
+ case: "topic",
31
+ value: {
32
+ schema: encodeJsonSchema(options.schema),
33
+ ordered: options.ordered ?? false,
34
+ retry: encodeRetryPolicy(options.retry),
35
+ },
36
+ },
37
+ source: declarationSite(),
38
+ }));
39
+ }
40
+ }
41
+ ensureClient(operation) {
42
+ if (unprovisionedPhase()) {
43
+ throw unprovisioned(`topic("${this.name}")`, operation);
44
+ }
45
+ const refusal = refuseUnbound(this.name, "topic");
46
+ if (refusal) {
47
+ throw refusal;
48
+ }
49
+ return (this.client ??= createClient(TopicService, createRuntimeTransport()));
50
+ }
51
+ /** Sends `payload` to every consumer of this topic, and answers the message's id. */
52
+ async send(payload, options = {}) {
53
+ const client = this.ensureClient("send");
54
+ const { messageId } = await client.send({
55
+ topic: this.name,
56
+ payload: encodePayload(payload),
57
+ dueAt: encodeDueAt(options.delay),
58
+ idempotencyKey: options.idempotencyKey ?? "",
59
+ key: options.key ?? "",
60
+ lane: encodeLane(options.lane),
61
+ });
62
+ return messageId;
63
+ }
64
+ declareConsumer(name, run, options, batch) {
65
+ registerRun({
66
+ kind: "consumer",
67
+ topic: this.name,
68
+ name,
69
+ worker: options.worker?.name ?? DEFAULT_WORKER,
70
+ batch: batch !== undefined,
71
+ schema: this.schema,
72
+ run,
73
+ hooks: {},
74
+ });
75
+ if (process.env.OCEL_PHASE === "discovery") {
76
+ defer(rpc.resource.declare({
77
+ resource: { name, type: ResourceType.CONSUMER },
78
+ config: {
79
+ case: "consumer",
80
+ value: {
81
+ topic: this.name,
82
+ worker: options.worker?.name ?? "",
83
+ retry: encodeRetryPolicy(options.retry),
84
+ concurrency: options.concurrency ?? 0,
85
+ maxDuration: encodeDuration(options.maxDuration),
86
+ lanes: (options.lanes ?? []).map(encodeLane),
87
+ batch: batch && { size: batch.size, timeout: encodeDuration(batch.timeout) },
88
+ },
89
+ },
90
+ source: declarationSite(),
91
+ }));
92
+ }
93
+ return { name, topic: this.name };
94
+ }
95
+ /** Declares a consumer that receives every message sent to this topic, one at a time. */
96
+ consumer(name, run, options = {}) {
97
+ return this.declareConsumer(name, run, options, undefined);
98
+ }
99
+ /** Declares a consumer that receives the messages sent to this topic in batches. */
100
+ batchConsumer(name, run, options) {
101
+ const { batchSize, batchTimeout, ...rest } = options;
102
+ return this.declareConsumer(name, run, rest, {
103
+ size: batchSize,
104
+ timeout: batchTimeout,
105
+ });
106
+ }
107
+ /** The messages a consumer of this topic gave up on. */
108
+ deadLetter(consumer) {
109
+ return new DeadLetters(typeof consumer === "string" ? consumer : consumer.name, (operation) => ({
110
+ client: this.ensureClient(operation),
111
+ topic: this.name,
112
+ }));
113
+ }
114
+ }
115
+ export function topic(name, options = {}) {
116
+ return new Topic(name, options);
117
+ }
@@ -0,0 +1,2 @@
1
+ /** The version of this SDK, stamped at release. */
2
+ export declare const SDK_VERSION = "0.0.1";
@@ -0,0 +1,2 @@
1
+ /** The version of this SDK, stamped at release. */
2
+ export const SDK_VERSION = "0.0.1";
@@ -0,0 +1,36 @@
1
+ /** What a run, its hooks and every middleware know about the attempt in progress. */
2
+ export interface RunContext {
3
+ /** Whether a task's run or a topic consumer is being served. */
4
+ kind: "task" | "consumer";
5
+ /** The task's name, or the consumer's. */
6
+ name: string;
7
+ /** The topic the message was sent to; a task's topic is the task's own name. */
8
+ topic: string;
9
+ /** The run's id for a task, the execution's for a consumer; a batch's first message's. */
10
+ id: string;
11
+ /** Which attempt this is. */
12
+ attempt: {
13
+ /** This attempt's number, the first being 1. */
14
+ number: number;
15
+ /** How many attempts are made at most. */
16
+ of: number;
17
+ /** When the first attempt started. */
18
+ firstAttemptedAt: Date | undefined;
19
+ };
20
+ /** The message being served; a batch's first message. */
21
+ message: {
22
+ /** The message's id. */
23
+ id: string;
24
+ /** When the message was sent. */
25
+ publishedAt: Date | undefined;
26
+ };
27
+ /** Aborted when the run is canceled while this attempt is in progress. */
28
+ signal: AbortSignal;
29
+ }
30
+ /** The second argument a run or a consumer receives. */
31
+ export interface RunOptions {
32
+ /** The attempt in progress. */
33
+ ctx: RunContext;
34
+ /** Aborted when the run is canceled while this attempt is in progress. */
35
+ signal: AbortSignal;
36
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ /** The HTTP answer `deliver` gives the message's sender. */
2
+ export interface DeliveryAnswer {
3
+ /** 200 on success, 422 to fail without retry, 500 to retry, 400 or 404 for a bad delivery. */
4
+ status: number;
5
+ /** The run's output as JSON, the abort as JSON, or a plain-text reason. */
6
+ body: string;
7
+ }
8
+ /**
9
+ * Serves one message the worker named `workerName` received: `body` is the envelope POSTed
10
+ * to it, and `signal` aborts when the run is canceled. Answers what to send back: 200 with
11
+ * the output, 422 to fail without retry, 500 to retry, 400 or 404 for a bad delivery.
12
+ */
13
+ export declare function deliver(workerName: string, body: string, signal?: AbortSignal): Promise<DeliveryAnswer>;
@@ -0,0 +1,258 @@
1
+ import { fromJson } from "@bufbuild/protobuf";
2
+ import { timestampDate } from "@bufbuild/protobuf/wkt";
3
+ import { validatePayload } from "../delivery/schema.js";
4
+ import { EnvelopeSchema } from "../gen/proto/app/topic/v1/topic_pb.js";
5
+ import { AbortTaskRunError } from "../task/errors.js";
6
+ import { findRegistration, findWorker } from "./registry.js";
7
+ import { Semaphore } from "./semaphore.js";
8
+ const describeError = (error) => (error instanceof Error ? error.message : String(error));
9
+ const createAbortAnswer = (reason) => ({
10
+ status: 422,
11
+ body: JSON.stringify({ abort: { reason } }),
12
+ });
13
+ const decodeTimestamp = (timestamp) => timestamp ? timestampDate(timestamp) : undefined;
14
+ function parseEnvelope(body) {
15
+ const parsed = JSON.parse(body);
16
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
17
+ throw new Error("the envelope is not a JSON object");
18
+ }
19
+ const { payload, messages, ...fields } = parsed;
20
+ const deliveries = Array.isArray(messages) ? messages : [];
21
+ const bare = Array.isArray(messages)
22
+ ? deliveries.map(({ payload: _payload, ...delivery }) => delivery)
23
+ : messages;
24
+ const envelope = fromJson(EnvelopeSchema, (bare === undefined ? fields : { ...fields, messages: bare }), { ignoreUnknownFields: true });
25
+ const payloads = deliveries.length > 0
26
+ ? deliveries.map((delivery) => delivery.payload ?? null)
27
+ : [payload ?? null];
28
+ return { envelope, payloads };
29
+ }
30
+ const starts = new Map();
31
+ const semaphores = new Map();
32
+ function ensureStarted(workerName) {
33
+ const onStart = findWorker(workerName)?.onStart;
34
+ if (!onStart)
35
+ return Promise.resolve();
36
+ let start = starts.get(workerName);
37
+ if (!start) {
38
+ start = Promise.resolve()
39
+ .then(onStart)
40
+ .catch((error) => {
41
+ starts.delete(workerName);
42
+ throw error;
43
+ });
44
+ starts.set(workerName, start);
45
+ }
46
+ return start;
47
+ }
48
+ function ensureSemaphore(workerName) {
49
+ const concurrency = findWorker(workerName)?.concurrency;
50
+ if (!concurrency || concurrency < 1)
51
+ return undefined;
52
+ let semaphore = semaphores.get(workerName);
53
+ if (!semaphore) {
54
+ semaphore = new Semaphore(concurrency);
55
+ semaphores.set(workerName, semaphore);
56
+ }
57
+ return semaphore;
58
+ }
59
+ function reportHookError(hook, registration, error) {
60
+ console.error(`ocel: ${registration.kind} "${registration.name}" ${hook} threw`, error);
61
+ }
62
+ async function callHook(hook, registration, call) {
63
+ try {
64
+ await call();
65
+ }
66
+ catch (error) {
67
+ reportHookError(hook, registration, error);
68
+ }
69
+ }
70
+ function createRunContext(registration, envelope, signal) {
71
+ const first = envelope.messages[0];
72
+ const message = first?.message ?? envelope.message;
73
+ const attempt = first?.attempt ?? envelope.attempt;
74
+ return {
75
+ kind: registration.kind,
76
+ name: registration.name,
77
+ topic: registration.topic,
78
+ id: first?.execution ?? envelope.execution,
79
+ attempt: {
80
+ number: attempt?.number ?? 1,
81
+ of: attempt?.of ?? 1,
82
+ firstAttemptedAt: decodeTimestamp(attempt?.firstAttemptedAt),
83
+ },
84
+ message: { id: message?.id ?? "", publishedAt: decodeTimestamp(message?.publishedAt) },
85
+ signal,
86
+ };
87
+ }
88
+ async function decodePayload(registration, { envelope, payloads: values }) {
89
+ const isBatchEnvelope = envelope.messages.length > 0;
90
+ if (!registration.batch && isBatchEnvelope) {
91
+ return {
92
+ ok: false,
93
+ answer: {
94
+ status: 400,
95
+ body: `${registration.kind} "${registration.name}" takes one message at a time, and this envelope carries a batch`,
96
+ },
97
+ };
98
+ }
99
+ const { schema } = registration;
100
+ if (!schema)
101
+ return { ok: true, payload: registration.batch ? values : values[0] };
102
+ const validated = [];
103
+ for (const value of values) {
104
+ const result = await validatePayload(schema, value);
105
+ if (!result.ok) {
106
+ return {
107
+ ok: false,
108
+ answer: createAbortAnswer(`the payload does not match the schema: ${result.message}`),
109
+ };
110
+ }
111
+ validated.push(result.value);
112
+ }
113
+ return { ok: true, payload: registration.batch ? validated : validated[0] };
114
+ }
115
+ async function serveAttempt(registration, workerName, delivered, signal) {
116
+ const parsed = await decodePayload(registration, delivered);
117
+ if (!parsed.ok)
118
+ return parsed.answer;
119
+ const { payload } = parsed;
120
+ const ctx = createRunContext(registration, delivered.envelope, signal);
121
+ const { hooks } = registration;
122
+ const workerMiddleware = findWorker(workerName)?.middleware;
123
+ let canceled = false;
124
+ const onAbort = () => {
125
+ canceled = true;
126
+ if (hooks.onCancel) {
127
+ void callHook("onCancel", registration, () => hooks.onCancel?.({ payload, ctx }));
128
+ }
129
+ };
130
+ if (signal.aborted)
131
+ onAbort();
132
+ else
133
+ signal.addEventListener("abort", onAbort, { once: true });
134
+ let output;
135
+ const runAttempt = async () => {
136
+ await hooks.onStartAttempt?.({ payload, ctx });
137
+ const run = async () => {
138
+ output = await registration.run(payload, { ctx, signal });
139
+ };
140
+ if (hooks.middleware)
141
+ await hooks.middleware({ payload, ctx, next: run });
142
+ else
143
+ await run();
144
+ };
145
+ let failure;
146
+ try {
147
+ if (workerMiddleware)
148
+ await workerMiddleware({ ctx, next: runAttempt });
149
+ else
150
+ await runAttempt();
151
+ }
152
+ catch (error) {
153
+ failure = { error };
154
+ }
155
+ finally {
156
+ signal.removeEventListener("abort", onAbort);
157
+ }
158
+ let body = "null";
159
+ if (!failure) {
160
+ try {
161
+ body = JSON.stringify(output ?? null) ?? "null";
162
+ }
163
+ catch (error) {
164
+ failure = {
165
+ error: new AbortTaskRunError(`the output does not encode as JSON: ${describeError(error)}`),
166
+ };
167
+ }
168
+ }
169
+ if (!failure) {
170
+ if (!canceled)
171
+ await callCompletionHooks(registration, { payload, ctx, result: { ok: true, output } });
172
+ return { status: 200, body };
173
+ }
174
+ const { error } = failure;
175
+ let abort = error instanceof AbortTaskRunError;
176
+ if (!abort && hooks.catchError) {
177
+ try {
178
+ const answer = (await hooks.catchError({ payload, error, ctx }));
179
+ abort = answer?.skipRetrying === true;
180
+ }
181
+ catch (hookError) {
182
+ reportHookError("catchError", registration, hookError);
183
+ }
184
+ }
185
+ if (!canceled && (abort || ctx.attempt.number >= ctx.attempt.of)) {
186
+ await callCompletionHooks(registration, { payload, ctx, result: { ok: false, error } });
187
+ }
188
+ return abort
189
+ ? createAbortAnswer(describeError(error))
190
+ : { status: 500, body: describeError(error) };
191
+ }
192
+ async function callCompletionHooks(registration, args) {
193
+ const { hooks } = registration;
194
+ const { payload, ctx, result } = args;
195
+ if (result.ok) {
196
+ if (hooks.onSuccess) {
197
+ await callHook("onSuccess", registration, () => hooks.onSuccess?.({ payload, output: result.output, ctx }));
198
+ }
199
+ }
200
+ else if (hooks.onFailure) {
201
+ await callHook("onFailure", registration, () => hooks.onFailure?.({ payload, error: result.error, ctx }));
202
+ }
203
+ if (hooks.onComplete) {
204
+ await callHook("onComplete", registration, () => hooks.onComplete?.({ payload, ctx, result }));
205
+ }
206
+ }
207
+ /**
208
+ * Serves one message the worker named `workerName` received: `body` is the envelope POSTed
209
+ * to it, and `signal` aborts when the run is canceled. Answers what to send back: 200 with
210
+ * the output, 422 to fail without retry, 500 to retry, 400 or 404 for a bad delivery.
211
+ */
212
+ export async function deliver(workerName, body, signal = new AbortController().signal) {
213
+ let delivered;
214
+ try {
215
+ delivered = parseEnvelope(body);
216
+ }
217
+ catch (error) {
218
+ return { status: 400, body: `the body is not a message envelope: ${describeError(error)}` };
219
+ }
220
+ const { envelope } = delivered;
221
+ const registration = findRegistration(envelope.topic, envelope.consumer);
222
+ if (!registration) {
223
+ return {
224
+ status: 404,
225
+ body: `no consumer "${envelope.consumer}" of topic "${envelope.topic}" is declared in this app`,
226
+ };
227
+ }
228
+ if (registration.worker !== workerName) {
229
+ return {
230
+ status: 404,
231
+ body: `consumer "${envelope.consumer}" of topic "${envelope.topic}" runs on worker "${registration.worker}", not on worker "${workerName}"`,
232
+ };
233
+ }
234
+ try {
235
+ await ensureStarted(workerName);
236
+ }
237
+ catch (error) {
238
+ return { status: 500, body: `worker "${workerName}" failed to start: ${describeError(error)}` };
239
+ }
240
+ const semaphore = ensureSemaphore(workerName);
241
+ if (!semaphore)
242
+ return serveAttempt(registration, workerName, delivered, signal);
243
+ try {
244
+ await semaphore.acquire(signal);
245
+ }
246
+ catch {
247
+ return {
248
+ status: 500,
249
+ body: `the delivery was canceled while it waited for a free slot on worker "${workerName}"`,
250
+ };
251
+ }
252
+ try {
253
+ return await serveAttempt(registration, workerName, delivered, signal);
254
+ }
255
+ finally {
256
+ semaphore.release();
257
+ }
258
+ }
@@ -0,0 +1,4 @@
1
+ export type { RunContext, RunOptions } from "./context.js";
2
+ export { type DeliveryAnswer, deliver } from "./deliver.js";
3
+ export type { WorkerMiddleware } from "./registry.js";
4
+ export { Worker, type WorkerOptions, worker } from "./worker.js";
@@ -0,0 +1,2 @@
1
+ export { deliver } from "./deliver.js";
2
+ export { Worker, worker } from "./worker.js";
@@ -0,0 +1,28 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+ import type { TaskHooks } from "../task/hooks.js";
3
+ import type { RunContext, RunOptions } from "./context.js";
4
+ export declare const DEFAULT_WORKER = "worker";
5
+ export interface Registration {
6
+ kind: "task" | "consumer";
7
+ topic: string;
8
+ name: string;
9
+ worker: string;
10
+ batch: boolean;
11
+ schema: StandardSchemaV1 | undefined;
12
+ run: (payload: unknown, options: RunOptions) => unknown;
13
+ hooks: TaskHooks<unknown, unknown>;
14
+ }
15
+ /** Wraps every run a worker serves, of its tasks and its consumers alike; `next` runs it. */
16
+ export type WorkerMiddleware = (args: {
17
+ ctx: RunContext;
18
+ next: () => Promise<void>;
19
+ }) => unknown;
20
+ export interface WorkerRegistration {
21
+ concurrency: number | undefined;
22
+ onStart: (() => unknown) | undefined;
23
+ middleware: WorkerMiddleware | undefined;
24
+ }
25
+ export declare function registerRun(registration: Registration): void;
26
+ export declare function findRegistration(topic: string, consumer: string): Registration | undefined;
27
+ export declare function registerWorker(name: string, worker: WorkerRegistration): void;
28
+ export declare function findWorker(name: string): WorkerRegistration | undefined;
@@ -0,0 +1,22 @@
1
+ export const DEFAULT_WORKER = "worker";
2
+ const registryKey = Symbol.for("ocel.worker.registry");
3
+ function ensureRegistry() {
4
+ const global = globalThis;
5
+ global[registryKey] ??= { runs: new Map(), workers: new Map() };
6
+ return global[registryKey];
7
+ }
8
+ function formatRunKey(topic, consumer) {
9
+ return `${topic}\u0000${consumer}`;
10
+ }
11
+ export function registerRun(registration) {
12
+ ensureRegistry().runs.set(formatRunKey(registration.topic, registration.name), registration);
13
+ }
14
+ export function findRegistration(topic, consumer) {
15
+ return ensureRegistry().runs.get(formatRunKey(topic, consumer));
16
+ }
17
+ export function registerWorker(name, worker) {
18
+ ensureRegistry().workers.set(name, worker);
19
+ }
20
+ export function findWorker(name) {
21
+ return ensureRegistry().workers.get(name);
22
+ }
@@ -0,0 +1,7 @@
1
+ export declare class Semaphore {
2
+ private available;
3
+ private waiting;
4
+ constructor(permits: number);
5
+ acquire(signal: AbortSignal): Promise<void>;
6
+ release(): void;
7
+ }
@@ -0,0 +1,36 @@
1
+ export class Semaphore {
2
+ available;
3
+ waiting = [];
4
+ constructor(permits) {
5
+ this.available = permits;
6
+ }
7
+ acquire(signal) {
8
+ if (this.available > 0) {
9
+ this.available--;
10
+ return Promise.resolve();
11
+ }
12
+ if (signal.aborted)
13
+ return Promise.reject(signal.reason);
14
+ return new Promise((resolve, reject) => {
15
+ const takePermit = () => {
16
+ signal.removeEventListener("abort", stopWaiting);
17
+ resolve();
18
+ };
19
+ const stopWaiting = () => {
20
+ const index = this.waiting.indexOf(takePermit);
21
+ if (index !== -1)
22
+ this.waiting.splice(index, 1);
23
+ reject(signal.reason);
24
+ };
25
+ this.waiting.push(takePermit);
26
+ signal.addEventListener("abort", stopWaiting, { once: true });
27
+ });
28
+ }
29
+ release() {
30
+ const next = this.waiting.shift();
31
+ if (next)
32
+ next();
33
+ else
34
+ this.available++;
35
+ }
36
+ }
@@ -0,0 +1,21 @@
1
+ import { type WorkerMiddleware } from "./registry.js";
2
+ /** How a worker is declared. */
3
+ export interface WorkerOptions {
4
+ /** The most runs this worker serves at once, across all its tasks and consumers. */
5
+ concurrency?: number;
6
+ /** Runs once per process, before the first run it serves. */
7
+ onStart?: () => unknown;
8
+ /** Wraps every run this worker serves. */
9
+ middleware?: WorkerMiddleware;
10
+ }
11
+ /** A declared worker: the app that serves the tasks and consumers placed on it. */
12
+ export declare class Worker {
13
+ /** The name this worker was declared under, and the `ocel.json` app it joins. */
14
+ readonly name: string;
15
+ constructor(name: string, options: WorkerOptions);
16
+ }
17
+ /**
18
+ * Declares a worker. A task or consumer runs on it when declared with `worker: <this>`;
19
+ * without one, it runs on the worker named `worker`.
20
+ */
21
+ export declare function worker(name: string, options?: WorkerOptions): Worker;
@@ -0,0 +1,32 @@
1
+ import { declarationSite } from "../declaration/callsite.js";
2
+ import { defer } from "../declaration/defer.js";
3
+ import { ResourceType } from "../gen/proto/app/resources/v1/resources_pb.js";
4
+ import { rpc } from "../runtime/rpc.js";
5
+ import { registerWorker } from "./registry.js";
6
+ /** A declared worker: the app that serves the tasks and consumers placed on it. */
7
+ export class Worker {
8
+ /** The name this worker was declared under, and the `ocel.json` app it joins. */
9
+ name;
10
+ constructor(name, options) {
11
+ this.name = name;
12
+ registerWorker(name, {
13
+ concurrency: options.concurrency,
14
+ onStart: options.onStart,
15
+ middleware: options.middleware,
16
+ });
17
+ if (process.env.OCEL_PHASE === "discovery") {
18
+ defer(rpc.resource.declare({
19
+ resource: { name, type: ResourceType.WORKER },
20
+ config: { case: "worker", value: { concurrency: options.concurrency ?? 0 } },
21
+ source: declarationSite(),
22
+ }));
23
+ }
24
+ }
25
+ }
26
+ /**
27
+ * Declares a worker. A task or consumer runs on it when declared with `worker: <this>`;
28
+ * without one, it runs on the worker named `worker`.
29
+ */
30
+ export function worker(name, options = {}) {
31
+ return new Worker(name, options);
32
+ }