@storyshelf/queue-sqs 0.1.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 ADDED
@@ -0,0 +1,79 @@
1
+ # @storyshelf/queue-sqs
2
+
3
+ SQS capture job queue adapter for StoryShelf: pushes capture jobs to AWS SQS and leaves execution to a separately-assembled worker.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ nub add @storyshelf/queue-sqs
9
+ ```
10
+
11
+ or
12
+
13
+ ```sh
14
+ npm install @storyshelf/queue-sqs
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```ts
20
+ import { createSqsCaptureQueue } from "@storyshelf/queue-sqs";
21
+ import { createShelfRouter } from "@storyshelf/core";
22
+
23
+ const queue = createSqsCaptureQueue({
24
+ queueUrl: "https://sqs.us-east-1.amazonaws.com/123456789012/capture-jobs",
25
+ });
26
+
27
+ const app = createShelfRouter({
28
+ database, storage,
29
+ captureRunner: myRenderer,
30
+ captureQueue: queue,
31
+ });
32
+ ```
33
+
34
+ ## API
35
+
36
+ ### `SqsCaptureQueueOptions`
37
+
38
+ ```ts
39
+ interface SqsCaptureQueueOptions {
40
+ queueUrl: string; // required SQS queue URL
41
+ client?: SqsClient; // optional pre-configured client
42
+ logger?: Logger; // optional pino logger
43
+ }
44
+ ```
45
+
46
+ ### `createSqsCaptureQueue(options: SqsCaptureQueueOptions): CaptureQueue`
47
+
48
+ Creates an SQS-backed `CaptureQueue`. The returned adapter implements every method of the `CaptureQueue` interface (`enqueue`, `status`, `active`, `recent`). Jobs are submitted via `SendMessage` and retrieved via `ReceiveMessage`; messages are deleted after reading to prevent re-processing.
49
+
50
+ ## How it fits in
51
+
52
+ `queue-sqs` is the `queue` option for `createShelfRouter` in AWS/cloud deployments. It implements the same `CaptureQueue` interface as `@storyshelf/capture/queue` (InMemoryCaptureQueue), so switching between in-process and remote queues requires no changes to router or build logic.
53
+
54
+ See `docs/architecture.md` and ADR 0009.
55
+
56
+ ## SQS Queue Worker
57
+
58
+ A separate worker process (Node, Bun, etc.) polls the SQS queue and calls `executeCaptureJob` from `@storyshelf/core`:
59
+
60
+ ```ts
61
+ import { executeCaptureJob } from "@storyshelf/core";
62
+ import { SqsClient, ReceiveMessageCommand, DeleteMessageCommand } from "@aws-sdk/client-sqs";
63
+
64
+ while (true) {
65
+ const msg = await sqsClient.send(new ReceiveMessageCommand({ ... }));
66
+ const { buildId, reqId } = JSON.parse(msg.Body!);
67
+ await executeCaptureJob({ buildId, reqId }, jobOptions);
68
+ await sqsClient.send(new DeleteMessageCommand({ QueueUrl: queueUrl, ReceiptHandle: msg.ReceiptHandle }));
69
+ }
70
+ ```
71
+
72
+ ## Development
73
+
74
+ ```sh
75
+ nub run build # bundle with tsdown
76
+ nub run fmt # format with oxfmt
77
+ nub run lint # type-aware lint with oxlint
78
+ nub run test # vitest suite
79
+ ```
@@ -0,0 +1,30 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { SQSClient } from "@aws-sdk/client-sqs";
4
+ import { CaptureQueue } from "@storyshelf/core";
5
+ import { Logger } from "pino";
6
+ //#region src/index.d.ts
7
+ /** Options for configuring an SQS-backed CaptureQueue. */
8
+ interface SqsCaptureQueueOptions {
9
+ /** SQS queue URL. */
10
+ queueUrl: string;
11
+ /** Optional pre-configured SQSClient. */
12
+ client?: SQSClient;
13
+ /** Optional logger for queue diagnostics. */
14
+ logger?: Logger;
15
+ }
16
+ /**
17
+ * Create an SQS-backed `CaptureQueue`.
18
+ *
19
+ * Jobs are submitted via `SendMessage` and retrieved via `ReceiveMessage`;
20
+ * messages are deleted after reading to prevent re-processing.
21
+ * A separately-assembled worker polls the queue and calls
22
+ * `executeCaptureJob` from `@storyshelf/core`.
23
+ *
24
+ * @param options - SQS queue URL and optional client configuration.
25
+ * @returns A `CaptureQueue` satisfying the core interface.
26
+ */
27
+ declare function createSqsCaptureQueue(options: SqsCaptureQueueOptions): CaptureQueue;
28
+ //#endregion
29
+ export { SqsCaptureQueueOptions, createSqsCaptureQueue };
30
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,124 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { DeleteMessageCommand, ReceiveMessageCommand, SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
5
+ //#region src/index.ts
6
+ function parseBody(raw) {
7
+ return JSON.parse(raw);
8
+ }
9
+ function hasQueuedOrRunningStatus(body) {
10
+ const status = body.status ?? "queued";
11
+ return ["queued", "running"].includes(status);
12
+ }
13
+ function mapQueueEntry(raw) {
14
+ const body = parseBody(raw.Body ?? "{}");
15
+ return {
16
+ buildId: body.buildId ?? "unknown",
17
+ status: body.status ?? "queued",
18
+ queuedAt: body.queuedAt ?? (/* @__PURE__ */ new Date()).toISOString()
19
+ };
20
+ }
21
+ /**
22
+ * Create an SQS-backed `CaptureQueue`.
23
+ *
24
+ * Jobs are submitted via `SendMessage` and retrieved via `ReceiveMessage`;
25
+ * messages are deleted after reading to prevent re-processing.
26
+ * A separately-assembled worker polls the queue and calls
27
+ * `executeCaptureJob` from `@storyshelf/core`.
28
+ *
29
+ * @param options - SQS queue URL and optional client configuration.
30
+ * @returns A `CaptureQueue` satisfying the core interface.
31
+ */
32
+ function createSqsCaptureQueue(options) {
33
+ const client = options.client ?? new SQSClient({});
34
+ return {
35
+ metadata: {
36
+ name: "SQS Queue",
37
+ version: "0.1.0",
38
+ description: "SQS-backed capture queue",
39
+ kind: "sqs"
40
+ },
41
+ /**
42
+ * Submit a build for capture. Resolves once the message is sent to SQS.
43
+ *
44
+ * The actual capture execution happens in a separate worker that
45
+ * polls the queue and calls `executeCaptureJob`.
46
+ */
47
+ async enqueue(job) {
48
+ await client.send(new SendMessageCommand({
49
+ QueueUrl: options.queueUrl,
50
+ MessageBody: JSON.stringify({
51
+ buildId: job.buildId,
52
+ reqId: job.reqId
53
+ }),
54
+ MessageAttributes: {
55
+ buildId: {
56
+ DataType: "String",
57
+ StringValue: job.buildId
58
+ },
59
+ status: {
60
+ DataType: "String",
61
+ StringValue: "queued"
62
+ }
63
+ }
64
+ }));
65
+ },
66
+ /**
67
+ * Return the current status entry for a build, or null if untracked.
68
+ *
69
+ * Polls the SQS queue for a message matching the buildId. If found,
70
+ * the message is deleted so it is not re-processed.
71
+ */
72
+ async status(buildId) {
73
+ const messages = (await client.send(new ReceiveMessageCommand({
74
+ QueueUrl: options.queueUrl,
75
+ MaxNumberOfMessages: 1,
76
+ MessageAttributeNames: ["All"]
77
+ }))).Messages ?? [];
78
+ if (messages.length === 0) return null;
79
+ const [msg] = messages;
80
+ if (!msg?.Body) return null;
81
+ const body = parseBody(msg.Body);
82
+ await client.send(new DeleteMessageCommand({
83
+ QueueUrl: options.queueUrl,
84
+ ReceiptHandle: msg.ReceiptHandle
85
+ }));
86
+ return {
87
+ buildId: body.buildId ?? buildId,
88
+ status: body.status ?? "queued",
89
+ queuedAt: body.queuedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
90
+ startedAt: body.startedAt,
91
+ finishedAt: body.finishedAt,
92
+ error: body.error
93
+ };
94
+ },
95
+ /**
96
+ * Return queue entries that are queued or running, newest first.
97
+ *
98
+ * Short poll for up to 10 messages. Filters by status.
99
+ */
100
+ async active() {
101
+ return ((await client.send(new ReceiveMessageCommand({
102
+ QueueUrl: options.queueUrl,
103
+ MaxNumberOfMessages: 10,
104
+ MessageAttributeNames: ["All"]
105
+ }))).Messages ?? []).filter((message) => message.Body?.length && hasQueuedOrRunningStatus(parseBody(message.Body))).map((msg) => mapQueueEntry(msg)).toSorted((left, right) => right.queuedAt.localeCompare(left.queuedAt));
106
+ },
107
+ /**
108
+ * Return the most recent queue entries, newest first.
109
+ *
110
+ * Short poll for up to `limit` messages, sorted by queuedAt descending.
111
+ */
112
+ async recent(limit) {
113
+ return ((await client.send(new ReceiveMessageCommand({
114
+ QueueUrl: options.queueUrl,
115
+ MaxNumberOfMessages: limit,
116
+ MessageAttributeNames: ["All"]
117
+ }))).Messages ?? []).map((msg) => mapQueueEntry(msg)).toSorted((left, right) => right.queuedAt.localeCompare(left.queuedAt));
118
+ }
119
+ };
120
+ }
121
+ //#endregion
122
+ export { createSqsCaptureQueue };
123
+
124
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import {\n DeleteMessageCommand,\n ReceiveMessageCommand,\n SendMessageCommand,\n SQSClient,\n} from \"@aws-sdk/client-sqs\";\n\nimport type {\n CaptureJob,\n CaptureQueue,\n JobStatus,\n QueueEntry,\n} from \"@storyshelf/core\";\n\nimport type { Logger } from \"pino\";\n\ndeclare const __PKG_VERSION__: string;\n\n/** Options for configuring an SQS-backed CaptureQueue. */\nexport interface SqsCaptureQueueOptions {\n /** SQS queue URL. */\n queueUrl: string;\n /** Optional pre-configured SQSClient. */\n client?: SQSClient;\n /** Optional logger for queue diagnostics. */\n logger?: Logger;\n}\n\ninterface QueuedBody {\n buildId?: string;\n status?: JobStatus;\n queuedAt?: string;\n startedAt?: string;\n finishedAt?: string;\n error?: string;\n reqId?: string;\n}\n\nfunction parseBody(raw: string): QueuedBody {\n return JSON.parse(raw) as QueuedBody;\n}\n\nfunction hasQueuedOrRunningStatus(body: QueuedBody): boolean {\n const status = body.status ?? \"queued\";\n return [\"queued\", \"running\"].includes(status);\n}\n\nfunction mapQueueEntry(raw: { Body?: string }): QueueEntry {\n const body = parseBody(raw.Body ?? \"{}\");\n return {\n buildId: body.buildId ?? \"unknown\",\n status: body.status ?? \"queued\",\n queuedAt: body.queuedAt ?? new Date().toISOString(),\n };\n}\n\n/**\n * Create an SQS-backed `CaptureQueue`.\n *\n * Jobs are submitted via `SendMessage` and retrieved via `ReceiveMessage`;\n * messages are deleted after reading to prevent re-processing.\n * A separately-assembled worker polls the queue and calls\n * `executeCaptureJob` from `@storyshelf/core`.\n *\n * @param options - SQS queue URL and optional client configuration.\n * @returns A `CaptureQueue` satisfying the core interface.\n */\n/* oxlint-disable max-lines-per-function */\nexport function createSqsCaptureQueue(\n options: SqsCaptureQueueOptions,\n): CaptureQueue {\n const client = options.client ?? new SQSClient({});\n\n return {\n metadata: {\n name: \"SQS Queue\",\n version: typeof __PKG_VERSION__ === \"undefined\" ? \"0.0.0\" : __PKG_VERSION__, // oxlint-disable-line unicorn/no-typeof-undefined\n description: \"SQS-backed capture queue\",\n kind: \"sqs\",\n },\n /**\n * Submit a build for capture. Resolves once the message is sent to SQS.\n *\n * The actual capture execution happens in a separate worker that\n * polls the queue and calls `executeCaptureJob`.\n */\n async enqueue(job: CaptureJob): Promise<void> {\n await client.send(\n new SendMessageCommand({\n QueueUrl: options.queueUrl,\n MessageBody: JSON.stringify({\n buildId: job.buildId,\n reqId: job.reqId,\n }),\n MessageAttributes: {\n buildId: {\n DataType: \"String\",\n StringValue: job.buildId,\n },\n status: {\n DataType: \"String\",\n StringValue: \"queued\",\n },\n },\n }),\n );\n },\n\n /**\n * Return the current status entry for a build, or null if untracked.\n *\n * Polls the SQS queue for a message matching the buildId. If found,\n * the message is deleted so it is not re-processed.\n */\n async status(buildId: string): Promise<QueueEntry | null> {\n const resp = await client.send(\n new ReceiveMessageCommand({\n QueueUrl: options.queueUrl,\n MaxNumberOfMessages: 1,\n MessageAttributeNames: [\"All\"],\n }),\n );\n\n const messages = resp.Messages ?? [];\n if (messages.length === 0) {\n return null;\n }\n\n const [msg] = messages;\n if (!msg?.Body) {\n return null;\n }\n\n const body = parseBody(msg.Body);\n\n await client.send(\n new DeleteMessageCommand({\n QueueUrl: options.queueUrl,\n ReceiptHandle: msg.ReceiptHandle,\n }),\n );\n\n return {\n buildId: body.buildId ?? buildId,\n status: body.status ?? \"queued\",\n queuedAt: body.queuedAt ?? new Date().toISOString(),\n startedAt: body.startedAt,\n finishedAt: body.finishedAt,\n error: body.error,\n };\n },\n\n /**\n * Return queue entries that are queued or running, newest first.\n *\n * Short poll for up to 10 messages. Filters by status.\n */\n async active(): Promise<QueueEntry[]> {\n const resp = await client.send(\n new ReceiveMessageCommand({\n QueueUrl: options.queueUrl,\n MaxNumberOfMessages: 10,\n MessageAttributeNames: [\"All\"],\n }),\n );\n\n return (resp.Messages ?? [])\n .filter((message) => message.Body?.length && hasQueuedOrRunningStatus(parseBody(message.Body)))\n .map((msg) => mapQueueEntry(msg))\n .toSorted((left, right) => right.queuedAt.localeCompare(left.queuedAt));\n },\n\n /**\n * Return the most recent queue entries, newest first.\n *\n * Short poll for up to `limit` messages, sorted by queuedAt descending.\n */\n async recent(limit: number): Promise<QueueEntry[]> {\n const resp = await client.send(\n new ReceiveMessageCommand({\n QueueUrl: options.queueUrl,\n MaxNumberOfMessages: limit,\n MessageAttributeNames: [\"All\"],\n }),\n );\n\n return (resp.Messages ?? [])\n .map((msg) => mapQueueEntry(msg))\n .toSorted((left, right) => right.queuedAt.localeCompare(left.queuedAt));\n },\n };\n}"],"mappings":";;;;;AAsCA,SAAS,UAAU,KAAyB;CAC1C,OAAO,KAAK,MAAM,GAAG;AACvB;AAEA,SAAS,yBAAyB,MAA2B;CAC3D,MAAM,SAAS,KAAK,UAAU;CAC9B,OAAO,CAAC,UAAU,SAAS,CAAC,CAAC,SAAS,MAAM;AAC9C;AAEA,SAAS,cAAc,KAAoC;CACzD,MAAM,OAAO,UAAU,IAAI,QAAQ,IAAI;CACvC,OAAO;EACL,SAAS,KAAK,WAAW;EACzB,QAAQ,KAAK,UAAU;EACvB,UAAU,KAAK,6BAAY,IAAI,KAAK,EAAA,CAAE,YAAY;CACpD;AACF;;;;;;;;;;;;AAcA,SAAgB,sBACd,SACc;CACd,MAAM,SAAS,QAAQ,UAAU,IAAI,UAAU,CAAC,CAAC;CAEjD,OAAO;EACL,UAAU;GACR,MAAM;GACN,SAAA;GACA,aAAa;GACb,MAAM;EACR;;;;;;;EAOA,MAAM,QAAQ,KAAgC;GAC5C,MAAM,OAAO,KACX,IAAI,mBAAmB;IACrB,UAAU,QAAQ;IAClB,aAAa,KAAK,UAAU;KAC1B,SAAS,IAAI;KACb,OAAO,IAAI;IACb,CAAC;IACD,mBAAmB;KACjB,SAAS;MACP,UAAU;MACV,aAAa,IAAI;KACnB;KACA,QAAQ;MACN,UAAU;MACV,aAAa;KACf;IACF;GACF,CAAC,CACH;EACF;;;;;;;EAQA,MAAM,OAAO,SAA6C;GASxD,MAAM,YAAW,MARE,OAAO,KACxB,IAAI,sBAAsB;IACxB,UAAU,QAAQ;IAClB,qBAAqB;IACrB,uBAAuB,CAAC,KAAK;GAC/B,CAAC,CACH,EAAA,CAEsB,YAAY,CAAC;GACnC,IAAI,SAAS,WAAW,GACtB,OAAO;GAGT,MAAM,CAAC,OAAO;GACd,IAAI,CAAC,KAAK,MACR,OAAO;GAGT,MAAM,OAAO,UAAU,IAAI,IAAI;GAE/B,MAAM,OAAO,KACX,IAAI,qBAAqB;IACvB,UAAU,QAAQ;IAClB,eAAe,IAAI;GACrB,CAAC,CACH;GAEA,OAAO;IACL,SAAS,KAAK,WAAW;IACzB,QAAQ,KAAK,UAAU;IACvB,UAAU,KAAK,6BAAY,IAAI,KAAK,EAAA,CAAE,YAAY;IAClD,WAAW,KAAK;IAChB,YAAY,KAAK;IACjB,OAAO,KAAK;GACd;EACF;;;;;;EAOA,MAAM,SAAgC;GASpC,SAAQ,MARW,OAAO,KACxB,IAAI,sBAAsB;IACxB,UAAU,QAAQ;IAClB,qBAAqB;IACrB,uBAAuB,CAAC,KAAK;GAC/B,CAAC,CACH,EAAA,CAEa,YAAY,CAAC,EAAA,CACvB,QAAQ,YAAY,QAAQ,MAAM,UAAU,yBAAyB,UAAU,QAAQ,IAAI,CAAC,CAAC,CAAC,CAC9F,KAAK,QAAQ,cAAc,GAAG,CAAC,CAAC,CAChC,UAAU,MAAM,UAAU,MAAM,SAAS,cAAc,KAAK,QAAQ,CAAC;EAC1E;;;;;;EAOA,MAAM,OAAO,OAAsC;GASjD,SAAQ,MARW,OAAO,KACxB,IAAI,sBAAsB;IACxB,UAAU,QAAQ;IAClB,qBAAqB;IACrB,uBAAuB,CAAC,KAAK;GAC/B,CAAC,CACH,EAAA,CAEa,YAAY,CAAC,EAAA,CACvB,KAAK,QAAQ,cAAc,GAAG,CAAC,CAAC,CAChC,UAAU,MAAM,UAAU,MAAM,SAAS,cAAc,KAAK,QAAQ,CAAC;EAC1E;CACF;AACF"}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@storyshelf/queue-sqs",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "SQS capture job queue adapter for StoryShelf (AWS cloud deployments).",
6
+ "author": {
7
+ "name": "Siddhant Gupta",
8
+ "url": "https://guptasiddhant.com"
9
+ },
10
+ "license": "MIT",
11
+ "sideEffects": false,
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/GuptaSiddhant/storyshelf.git",
15
+ "directory": "packages/queue-sqs"
16
+ },
17
+ "homepage": "https://github.com/GuptaSiddhant/storyshelf#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/GuptaSiddhant/storyshelf/issues"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "exports": {
24
+ ".": "./dist/index.mjs",
25
+ "./package.json": "./package.json"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsdown",
33
+ "dev": "tsdown -w",
34
+ "fmt": "oxfmt -c ../../.oxfmtrc.json ./src",
35
+ "lint": "oxlint --type-aware --type-check ./src",
36
+ "test": "vitest run",
37
+ "prepublishOnly": "nub run build"
38
+ },
39
+ "dependencies": {
40
+ "@aws-sdk/client-sqs": "^3.700.0",
41
+ "@storyshelf/core": "workspace:*"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "catalog:",
45
+ "@vitest/coverage-v8": "catalog:",
46
+ "oxfmt": "catalog:",
47
+ "oxlint": "catalog:",
48
+ "pino": "catalog:",
49
+ "tsdown": "catalog:",
50
+ "typescript": "catalog:",
51
+ "vitest": "catalog:"
52
+ },
53
+ "types": "./dist/index.d.mts",
54
+ "exports": {
55
+ ".": {
56
+ "source": "./src/index.ts",
57
+ "default": "./dist/index.mjs"
58
+ },
59
+ "./package.json": "./package.json"
60
+ }
61
+ }