@classytic/arc 1.1.0 → 2.1.2
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 +247 -794
- package/bin/arc.js +91 -52
- package/dist/EventTransport-BD2U0BTc.d.mts +100 -0
- package/dist/EventTransport-BD2U0BTc.d.mts.map +1 -0
- package/dist/HookSystem-BsGV-j2l.mjs +405 -0
- package/dist/HookSystem-BsGV-j2l.mjs.map +1 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs +250 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs.map +1 -0
- package/dist/adapters/index.d.mts +5 -0
- package/dist/adapters/index.mjs +3 -0
- package/dist/audit/index.d.mts +82 -0
- package/dist/audit/index.d.mts.map +1 -0
- package/dist/audit/index.mjs +276 -0
- package/dist/audit/index.mjs.map +1 -0
- package/dist/audit/mongodb.d.mts +5 -0
- package/dist/audit/mongodb.mjs +3 -0
- package/dist/audited-C3T5DTUx.mjs +141 -0
- package/dist/audited-C3T5DTUx.mjs.map +1 -0
- package/dist/auth/index.d.mts +189 -0
- package/dist/auth/index.d.mts.map +1 -0
- package/dist/auth/index.mjs +1102 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/auth/redis-session.d.mts +44 -0
- package/dist/auth/redis-session.d.mts.map +1 -0
- package/dist/auth/redis-session.mjs +76 -0
- package/dist/auth/redis-session.mjs.map +1 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs +250 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +1 -0
- package/dist/cache/index.d.mts +146 -0
- package/dist/cache/index.d.mts.map +1 -0
- package/dist/cache/index.mjs +92 -0
- package/dist/cache/index.mjs.map +1 -0
- package/dist/caching-Bl28lYsR.mjs +94 -0
- package/dist/caching-Bl28lYsR.mjs.map +1 -0
- package/dist/chunk-C7Uep-_p.mjs +20 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs +1097 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs.map +1 -0
- package/dist/cli/commands/describe.d.mts +19 -0
- package/dist/cli/commands/describe.d.mts.map +1 -0
- package/dist/cli/commands/describe.mjs +239 -0
- package/dist/cli/commands/describe.mjs.map +1 -0
- package/dist/cli/commands/docs.d.mts +14 -0
- package/dist/cli/commands/docs.d.mts.map +1 -0
- package/dist/cli/commands/docs.mjs +53 -0
- package/dist/cli/commands/docs.mjs.map +1 -0
- package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -1
- package/dist/cli/commands/generate.d.mts.map +1 -0
- package/dist/cli/commands/generate.mjs +358 -0
- package/dist/cli/commands/generate.mjs.map +1 -0
- package/dist/cli/commands/{init.d.ts → init.d.mts} +12 -8
- package/dist/cli/commands/init.d.mts.map +1 -0
- package/dist/cli/commands/{init.js → init.mjs} +807 -616
- package/dist/cli/commands/init.mjs.map +1 -0
- package/dist/cli/commands/introspect.d.mts +11 -0
- package/dist/cli/commands/introspect.d.mts.map +1 -0
- package/dist/cli/commands/introspect.mjs +76 -0
- package/dist/cli/commands/introspect.mjs.map +1 -0
- package/dist/cli/index.d.mts +17 -0
- package/dist/cli/index.d.mts.map +1 -0
- package/dist/cli/index.mjs +157 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/constants-DdXFXQtN.mjs +85 -0
- package/dist/constants-DdXFXQtN.mjs.map +1 -0
- package/dist/core/index.d.mts +5 -0
- package/dist/core/index.mjs +4 -0
- package/dist/createApp-CUgNqegw.mjs +560 -0
- package/dist/createApp-CUgNqegw.mjs.map +1 -0
- package/dist/defineResource-k0_BDn8v.mjs +2197 -0
- package/dist/defineResource-k0_BDn8v.mjs.map +1 -0
- package/dist/discovery/index.d.mts +47 -0
- package/dist/discovery/index.d.mts.map +1 -0
- package/dist/discovery/index.mjs +110 -0
- package/dist/discovery/index.mjs.map +1 -0
- package/dist/docs/index.d.mts +163 -0
- package/dist/docs/index.d.mts.map +1 -0
- package/dist/docs/index.mjs +73 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/elevation-BRy3yFWT.mjs +113 -0
- package/dist/elevation-BRy3yFWT.mjs.map +1 -0
- package/dist/elevation-B_2dRLVP.d.mts +88 -0
- package/dist/elevation-B_2dRLVP.d.mts.map +1 -0
- package/dist/errorHandler-BbcgBmIH.d.mts +73 -0
- package/dist/errorHandler-BbcgBmIH.d.mts.map +1 -0
- package/dist/errorHandler-C1okiriz.mjs +109 -0
- package/dist/errorHandler-C1okiriz.mjs.map +1 -0
- package/dist/errors-B9bZok84.mjs +212 -0
- package/dist/errors-B9bZok84.mjs.map +1 -0
- package/dist/errors-ChKiFz62.d.mts +125 -0
- package/dist/errors-ChKiFz62.d.mts.map +1 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts +125 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts.map +1 -0
- package/dist/eventPlugin-DGR_B2on.mjs +230 -0
- package/dist/eventPlugin-DGR_B2on.mjs.map +1 -0
- package/dist/events/index.d.mts +54 -0
- package/dist/events/index.d.mts.map +1 -0
- package/dist/events/index.mjs +52 -0
- package/dist/events/index.mjs.map +1 -0
- package/dist/events/transports/redis-stream-entry.d.mts +2 -0
- package/dist/events/transports/redis-stream-entry.mjs +178 -0
- package/dist/events/transports/redis-stream-entry.mjs.map +1 -0
- package/dist/events/transports/redis.d.mts +77 -0
- package/dist/events/transports/redis.d.mts.map +1 -0
- package/dist/events/transports/redis.mjs +125 -0
- package/dist/events/transports/redis.mjs.map +1 -0
- package/dist/externalPaths-DlINfKbP.d.mts +51 -0
- package/dist/externalPaths-DlINfKbP.d.mts.map +1 -0
- package/dist/factory/index.d.mts +64 -0
- package/dist/factory/index.d.mts.map +1 -0
- package/dist/factory/index.mjs +3 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts +217 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +1 -0
- package/dist/fields-DyaDVX4J.d.mts +110 -0
- package/dist/fields-DyaDVX4J.d.mts.map +1 -0
- package/dist/fields-iagOozy0.mjs +115 -0
- package/dist/fields-iagOozy0.mjs.map +1 -0
- package/dist/hooks/index.d.mts +4 -0
- package/dist/hooks/index.mjs +3 -0
- package/dist/idempotency/index.d.mts +97 -0
- package/dist/idempotency/index.d.mts.map +1 -0
- package/dist/idempotency/index.mjs +320 -0
- package/dist/idempotency/index.mjs.map +1 -0
- package/dist/idempotency/mongodb.d.mts +2 -0
- package/dist/idempotency/mongodb.mjs +115 -0
- package/dist/idempotency/mongodb.mjs.map +1 -0
- package/dist/idempotency/redis.d.mts +2 -0
- package/dist/idempotency/redis.mjs +104 -0
- package/dist/idempotency/redis.mjs.map +1 -0
- package/dist/index.d.mts +261 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +105 -0
- package/dist/index.mjs.map +1 -0
- package/dist/integrations/event-gateway.d.mts +47 -0
- package/dist/integrations/event-gateway.d.mts.map +1 -0
- package/dist/integrations/event-gateway.mjs +44 -0
- package/dist/integrations/event-gateway.mjs.map +1 -0
- package/dist/integrations/index.d.mts +5 -0
- package/dist/integrations/index.mjs +1 -0
- package/dist/integrations/jobs.d.mts +104 -0
- package/dist/integrations/jobs.d.mts.map +1 -0
- package/dist/integrations/jobs.mjs +124 -0
- package/dist/integrations/jobs.mjs.map +1 -0
- package/dist/integrations/streamline.d.mts +61 -0
- package/dist/integrations/streamline.d.mts.map +1 -0
- package/dist/integrations/streamline.mjs +126 -0
- package/dist/integrations/streamline.mjs.map +1 -0
- package/dist/integrations/websocket.d.mts +83 -0
- package/dist/integrations/websocket.d.mts.map +1 -0
- package/dist/integrations/websocket.mjs +289 -0
- package/dist/integrations/websocket.mjs.map +1 -0
- package/dist/interface-B01JvPVc.d.mts +78 -0
- package/dist/interface-B01JvPVc.d.mts.map +1 -0
- package/dist/interface-CZe8IkMf.d.mts +55 -0
- package/dist/interface-CZe8IkMf.d.mts.map +1 -0
- package/dist/interface-Ch8HU9uM.d.mts +1098 -0
- package/dist/interface-Ch8HU9uM.d.mts.map +1 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs +54 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +1 -0
- package/dist/keys-BqNejWup.mjs +43 -0
- package/dist/keys-BqNejWup.mjs.map +1 -0
- package/dist/logger-Df2O2WsW.mjs +79 -0
- package/dist/logger-Df2O2WsW.mjs.map +1 -0
- package/dist/memory-cQgelFOj.mjs +144 -0
- package/dist/memory-cQgelFOj.mjs.map +1 -0
- package/dist/migrations/index.d.mts +157 -0
- package/dist/migrations/index.d.mts.map +1 -0
- package/dist/migrations/index.mjs +261 -0
- package/dist/migrations/index.mjs.map +1 -0
- package/dist/mongodb-BfJVlUJH.mjs +94 -0
- package/dist/mongodb-BfJVlUJH.mjs.map +1 -0
- package/dist/mongodb-CGzRbfAK.d.mts +119 -0
- package/dist/mongodb-CGzRbfAK.d.mts.map +1 -0
- package/dist/mongodb-JN-9JA7K.d.mts +72 -0
- package/dist/mongodb-JN-9JA7K.d.mts.map +1 -0
- package/dist/openapi-G3Cw7XuM.mjs +524 -0
- package/dist/openapi-G3Cw7XuM.mjs.map +1 -0
- package/dist/org/index.d.mts +69 -0
- package/dist/org/index.d.mts.map +1 -0
- package/dist/org/index.mjs +514 -0
- package/dist/org/index.mjs.map +1 -0
- package/dist/org/types.d.mts +83 -0
- package/dist/org/types.d.mts.map +1 -0
- package/dist/org/types.mjs +1 -0
- package/dist/permissions/index.d.mts +279 -0
- package/dist/permissions/index.d.mts.map +1 -0
- package/dist/permissions/index.mjs +579 -0
- package/dist/permissions/index.mjs.map +1 -0
- package/dist/plugins/index.d.mts +173 -0
- package/dist/plugins/index.d.mts.map +1 -0
- package/dist/plugins/index.mjs +523 -0
- package/dist/plugins/index.mjs.map +1 -0
- package/dist/plugins/response-cache.d.mts +88 -0
- package/dist/plugins/response-cache.d.mts.map +1 -0
- package/dist/plugins/response-cache.mjs +284 -0
- package/dist/plugins/response-cache.mjs.map +1 -0
- package/dist/plugins/tracing-entry.d.mts +2 -0
- package/dist/plugins/tracing-entry.mjs +186 -0
- package/dist/plugins/tracing-entry.mjs.map +1 -0
- package/dist/pluralize-CEweyOEm.mjs +87 -0
- package/dist/pluralize-CEweyOEm.mjs.map +1 -0
- package/dist/policies/{index.d.ts → index.d.mts} +204 -169
- package/dist/policies/index.d.mts.map +1 -0
- package/dist/policies/index.mjs +322 -0
- package/dist/policies/index.mjs.map +1 -0
- package/dist/presets/{index.d.ts → index.d.mts} +63 -131
- package/dist/presets/index.d.mts.map +1 -0
- package/dist/presets/index.mjs +144 -0
- package/dist/presets/index.mjs.map +1 -0
- package/dist/presets/multiTenant.d.mts +25 -0
- package/dist/presets/multiTenant.d.mts.map +1 -0
- package/dist/presets/multiTenant.mjs +114 -0
- package/dist/presets/multiTenant.mjs.map +1 -0
- package/dist/presets-BITljm96.mjs +120 -0
- package/dist/presets-BITljm96.mjs.map +1 -0
- package/dist/presets-DzSMwlKj.d.mts +58 -0
- package/dist/presets-DzSMwlKj.d.mts.map +1 -0
- package/dist/prisma-DJbMt3yf.mjs +628 -0
- package/dist/prisma-DJbMt3yf.mjs.map +1 -0
- package/dist/prisma-Dg9GoVdj.d.mts +275 -0
- package/dist/prisma-Dg9GoVdj.d.mts.map +1 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts +72 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts.map +1 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs +139 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +1 -0
- package/dist/redis-D-JAeLtm.d.mts +50 -0
- package/dist/redis-D-JAeLtm.d.mts.map +1 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts +104 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts.map +1 -0
- package/dist/registry/index.d.mts +12 -0
- package/dist/registry/index.d.mts.map +1 -0
- package/dist/registry/index.mjs +4 -0
- package/dist/requestContext-QQD6ROJc.mjs +56 -0
- package/dist/requestContext-QQD6ROJc.mjs.map +1 -0
- package/dist/schemaConverter-BwrmWroW.mjs +99 -0
- package/dist/schemaConverter-BwrmWroW.mjs.map +1 -0
- package/dist/schemas/index.d.mts +64 -0
- package/dist/schemas/index.d.mts.map +1 -0
- package/dist/schemas/index.mjs +83 -0
- package/dist/schemas/index.mjs.map +1 -0
- package/dist/scope/index.d.mts +22 -0
- package/dist/scope/index.d.mts.map +1 -0
- package/dist/scope/index.mjs +66 -0
- package/dist/scope/index.mjs.map +1 -0
- package/dist/sessionManager-jPKLbHE0.d.mts +187 -0
- package/dist/sessionManager-jPKLbHE0.d.mts.map +1 -0
- package/dist/sse-B3c3_yZp.mjs +124 -0
- package/dist/sse-B3c3_yZp.mjs.map +1 -0
- package/dist/testing/index.d.mts +908 -0
- package/dist/testing/index.d.mts.map +1 -0
- package/dist/testing/index.mjs +1977 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/tracing-Cc7vVQPp.d.mts +71 -0
- package/dist/tracing-Cc7vVQPp.d.mts.map +1 -0
- package/dist/typeGuards-DhMNLuvU.mjs +10 -0
- package/dist/typeGuards-DhMNLuvU.mjs.map +1 -0
- package/dist/types/index.d.mts +947 -0
- package/dist/types/index.d.mts.map +1 -0
- package/dist/types/index.mjs +15 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/types-Beqn1Un7.mjs +39 -0
- package/dist/types-Beqn1Un7.mjs.map +1 -0
- package/dist/types-CIgB7UUl.d.mts +446 -0
- package/dist/types-CIgB7UUl.d.mts.map +1 -0
- package/dist/types-aYB4V7uN.d.mts +87 -0
- package/dist/types-aYB4V7uN.d.mts.map +1 -0
- package/dist/utils/index.d.mts +748 -0
- package/dist/utils/index.d.mts.map +1 -0
- package/dist/utils/index.mjs +6 -0
- package/package.json +194 -68
- package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
- package/dist/adapters/index.d.ts +0 -237
- package/dist/adapters/index.js +0 -668
- package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
- package/dist/audit/index.d.ts +0 -195
- package/dist/audit/index.js +0 -319
- package/dist/auth/index.d.ts +0 -47
- package/dist/auth/index.js +0 -174
- package/dist/cli/commands/docs.d.ts +0 -11
- package/dist/cli/commands/docs.js +0 -474
- package/dist/cli/commands/generate.js +0 -334
- package/dist/cli/commands/introspect.d.ts +0 -8
- package/dist/cli/commands/introspect.js +0 -338
- package/dist/cli/index.d.ts +0 -4
- package/dist/cli/index.js +0 -3269
- package/dist/core/index.d.ts +0 -220
- package/dist/core/index.js +0 -2786
- package/dist/createApp-Ce9wl8W9.d.ts +0 -77
- package/dist/docs/index.d.ts +0 -166
- package/dist/docs/index.js +0 -658
- package/dist/errors-8WIxGS_6.d.ts +0 -122
- package/dist/events/index.d.ts +0 -117
- package/dist/events/index.js +0 -89
- package/dist/factory/index.d.ts +0 -38
- package/dist/factory/index.js +0 -1652
- package/dist/hooks/index.d.ts +0 -4
- package/dist/hooks/index.js +0 -199
- package/dist/idempotency/index.d.ts +0 -323
- package/dist/idempotency/index.js +0 -500
- package/dist/index-B4t03KQ0.d.ts +0 -1366
- package/dist/index.d.ts +0 -135
- package/dist/index.js +0 -4756
- package/dist/migrations/index.d.ts +0 -185
- package/dist/migrations/index.js +0 -274
- package/dist/org/index.d.ts +0 -129
- package/dist/org/index.js +0 -220
- package/dist/permissions/index.d.ts +0 -144
- package/dist/permissions/index.js +0 -103
- package/dist/plugins/index.d.ts +0 -46
- package/dist/plugins/index.js +0 -1069
- package/dist/policies/index.js +0 -196
- package/dist/presets/index.js +0 -384
- package/dist/presets/multiTenant.d.ts +0 -39
- package/dist/presets/multiTenant.js +0 -112
- package/dist/registry/index.d.ts +0 -16
- package/dist/registry/index.js +0 -253
- package/dist/testing/index.d.ts +0 -618
- package/dist/testing/index.js +0 -48020
- package/dist/types/index.d.ts +0 -4
- package/dist/types/index.js +0 -8
- package/dist/types-B99TBmFV.d.ts +0 -76
- package/dist/types-BvckRbs2.d.ts +0 -143
- package/dist/utils/index.d.ts +0 -679
- package/dist/utils/index.js +0 -931
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
//#region src/integrations/streamline.ts
|
|
2
|
+
const streamlinePluginImpl = async (fastify, options) => {
|
|
3
|
+
const { workflows, prefix = "/workflows", auth = true, bridgeEvents = true, permissions: perms } = options;
|
|
4
|
+
const registry = /* @__PURE__ */ new Map();
|
|
5
|
+
for (const wf of workflows) {
|
|
6
|
+
const id = wf.definition.id;
|
|
7
|
+
if (registry.has(id)) throw new Error(`Duplicate workflow ID: '${id}'`);
|
|
8
|
+
registry.set(id, wf);
|
|
9
|
+
}
|
|
10
|
+
if (!fastify.hasDecorator("workflows")) fastify.decorate("workflows", registry);
|
|
11
|
+
if (!fastify.hasDecorator("getWorkflow")) fastify.decorate("getWorkflow", (id) => registry.get(id) ?? null);
|
|
12
|
+
const authPreHandler = auth && typeof fastify.authenticate === "function" ? [fastify.authenticate] : [];
|
|
13
|
+
const checkPerm = async (op, request) => {
|
|
14
|
+
const check = perms?.[op];
|
|
15
|
+
if (!check) return true;
|
|
16
|
+
return check(request);
|
|
17
|
+
};
|
|
18
|
+
for (const [id, wf] of registry) {
|
|
19
|
+
const routePrefix = `${prefix}/${id}`;
|
|
20
|
+
fastify.post(`${routePrefix}/start`, { preHandler: authPreHandler }, async (request, reply) => {
|
|
21
|
+
if (!await checkPerm("start", request)) return reply.status(403).send({
|
|
22
|
+
success: false,
|
|
23
|
+
error: "Forbidden"
|
|
24
|
+
});
|
|
25
|
+
const { input, meta } = request.body ?? {};
|
|
26
|
+
const run = await wf.start(input, meta);
|
|
27
|
+
if (bridgeEvents && fastify.events?.publish) await fastify.events.publish(`workflow.${id}.started`, {
|
|
28
|
+
runId: run._id,
|
|
29
|
+
workflowId: id,
|
|
30
|
+
status: run.status
|
|
31
|
+
});
|
|
32
|
+
return reply.status(201).send({
|
|
33
|
+
success: true,
|
|
34
|
+
data: run
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
fastify.get(`${routePrefix}/runs/:runId`, { preHandler: authPreHandler }, async (request, reply) => {
|
|
38
|
+
if (!await checkPerm("get", request)) return reply.status(403).send({
|
|
39
|
+
success: false,
|
|
40
|
+
error: "Forbidden"
|
|
41
|
+
});
|
|
42
|
+
const { runId } = request.params;
|
|
43
|
+
const run = await wf.get(runId);
|
|
44
|
+
if (!run) return reply.status(404).send({
|
|
45
|
+
success: false,
|
|
46
|
+
error: "Workflow run not found"
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
success: true,
|
|
50
|
+
data: run
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
fastify.post(`${routePrefix}/runs/:runId/resume`, { preHandler: authPreHandler }, async (request, reply) => {
|
|
54
|
+
if (!await checkPerm("resume", request)) return reply.status(403).send({
|
|
55
|
+
success: false,
|
|
56
|
+
error: "Forbidden"
|
|
57
|
+
});
|
|
58
|
+
const { runId } = request.params;
|
|
59
|
+
const { payload } = request.body ?? {};
|
|
60
|
+
const run = await wf.resume(runId, payload);
|
|
61
|
+
if (bridgeEvents && fastify.events?.publish) await fastify.events.publish(`workflow.${id}.resumed`, {
|
|
62
|
+
runId: run._id,
|
|
63
|
+
workflowId: id,
|
|
64
|
+
status: run.status
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
data: run
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
fastify.post(`${routePrefix}/runs/:runId/cancel`, { preHandler: authPreHandler }, async (request, reply) => {
|
|
72
|
+
if (!await checkPerm("cancel", request)) return reply.status(403).send({
|
|
73
|
+
success: false,
|
|
74
|
+
error: "Forbidden"
|
|
75
|
+
});
|
|
76
|
+
const { runId } = request.params;
|
|
77
|
+
const run = await wf.cancel(runId);
|
|
78
|
+
if (bridgeEvents && fastify.events?.publish) await fastify.events.publish(`workflow.${id}.cancelled`, {
|
|
79
|
+
runId: run._id,
|
|
80
|
+
workflowId: id
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
success: true,
|
|
84
|
+
data: run
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
if (wf.engine.pause) fastify.post(`${routePrefix}/runs/:runId/pause`, { preHandler: authPreHandler }, async (request, reply) => {
|
|
88
|
+
const { runId } = request.params;
|
|
89
|
+
return {
|
|
90
|
+
success: true,
|
|
91
|
+
data: await wf.engine.pause(runId)
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
if (wf.engine.rewindTo) fastify.post(`${routePrefix}/runs/:runId/rewind`, { preHandler: authPreHandler }, async (request, reply) => {
|
|
95
|
+
const { runId } = request.params;
|
|
96
|
+
const { stepId } = request.body ?? {};
|
|
97
|
+
if (!stepId) return reply.status(400).send({
|
|
98
|
+
success: false,
|
|
99
|
+
error: "stepId is required"
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
data: await wf.engine.rewindTo(runId, stepId)
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
fastify.get(prefix, { preHandler: authPreHandler }, async () => {
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
data: Array.from(registry.entries()).map(([id, wf]) => ({
|
|
111
|
+
id,
|
|
112
|
+
name: wf.definition.name ?? id,
|
|
113
|
+
steps: Object.keys(wf.definition.steps)
|
|
114
|
+
}))
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
fastify.addHook("onClose", async () => {
|
|
118
|
+
for (const wf of registry.values()) wf.shutdown?.();
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
/** Pluggable streamline integration for Arc */
|
|
122
|
+
const streamlinePlugin = streamlinePluginImpl;
|
|
123
|
+
|
|
124
|
+
//#endregion
|
|
125
|
+
export { streamlinePlugin as default, streamlinePlugin };
|
|
126
|
+
//# sourceMappingURL=streamline.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streamline.mjs","names":[],"sources":["../../src/integrations/streamline.ts"],"sourcesContent":["/**\n * @classytic/arc — Streamline Integration\n *\n * Pluggable adapter that wires @classytic/streamline workflows into Arc's\n * Fastify application. Provides REST endpoints for workflow management,\n * auto-connects to Arc's event bus, and respects Arc's auth/permissions.\n *\n * This is a SEPARATE subpath import — only loaded when explicitly used:\n * import { streamlinePlugin } from '@classytic/arc/integrations/streamline';\n *\n * Requires: @classytic/streamline (peer dependency)\n *\n * @example\n * ```typescript\n * import { streamlinePlugin } from '@classytic/arc/integrations/streamline';\n * import { orderWorkflow } from './workflows/order.js';\n *\n * await fastify.register(streamlinePlugin, {\n * workflows: [orderWorkflow],\n * prefix: '/api/workflows',\n * auth: true, // require authentication for workflow endpoints\n * });\n *\n * // Starts the workflow\n * // POST /api/workflows/order/start { input }\n * // GET /api/workflows/order/runs/:runId\n * // POST /api/workflows/order/runs/:runId/resume { payload }\n * // POST /api/workflows/order/runs/:runId/cancel\n * // GET /api/workflows/order/runs (list runs)\n * ```\n */\nimport type { FastifyInstance, FastifyPluginAsync } from 'fastify';\n\n// ============================================================================\n// Types (defined here so we don't import streamline at module level)\n// ============================================================================\n\n/** Minimal workflow interface — matches @classytic/streamline's createWorkflow() return */\nexport interface WorkflowLike {\n definition: { id: string; name?: string; steps: Record<string, unknown> };\n engine: {\n start(input: unknown, meta?: unknown): Promise<WorkflowRunLike>;\n execute(runId: string): Promise<WorkflowRunLike>;\n resume(runId: string, payload?: unknown): Promise<WorkflowRunLike>;\n cancel(runId: string): Promise<WorkflowRunLike>;\n pause?(runId: string): Promise<WorkflowRunLike>;\n rewindTo?(runId: string, stepId: string): Promise<WorkflowRunLike>;\n get(runId: string): Promise<WorkflowRunLike | null>;\n shutdown?(): void;\n };\n start(input: unknown, meta?: unknown): Promise<WorkflowRunLike>;\n resume(runId: string, payload?: unknown): Promise<WorkflowRunLike>;\n cancel(runId: string): Promise<WorkflowRunLike>;\n get(runId: string): Promise<WorkflowRunLike | null>;\n shutdown?(): void;\n}\n\nexport interface WorkflowRunLike {\n _id: string;\n workflowId: string;\n status: string;\n context?: unknown;\n input?: unknown;\n steps?: Record<string, unknown>;\n error?: unknown;\n createdAt?: Date;\n updatedAt?: Date;\n [key: string]: unknown;\n}\n\nexport interface StreamlinePluginOptions {\n /** Array of workflows created with createWorkflow() */\n workflows: WorkflowLike[];\n /** URL prefix for workflow endpoints (default: '/workflows') */\n prefix?: string;\n /** Require authentication for all workflow endpoints (default: true) */\n auth?: boolean;\n /** Connect workflow events to Arc's event bus (default: true) */\n bridgeEvents?: boolean;\n /** Custom permission check for workflow operations */\n permissions?: {\n start?: (request: unknown) => boolean | Promise<boolean>;\n resume?: (request: unknown) => boolean | Promise<boolean>;\n cancel?: (request: unknown) => boolean | Promise<boolean>;\n list?: (request: unknown) => boolean | Promise<boolean>;\n get?: (request: unknown) => boolean | Promise<boolean>;\n };\n}\n\n// ============================================================================\n// Plugin Implementation\n// ============================================================================\n\nconst streamlinePluginImpl: FastifyPluginAsync<StreamlinePluginOptions> = async (\n fastify: FastifyInstance,\n options: StreamlinePluginOptions\n) => {\n const {\n workflows,\n prefix = '/workflows',\n auth = true,\n bridgeEvents = true,\n permissions: perms,\n } = options;\n\n // Registry: workflowId → workflow instance\n const registry = new Map<string, WorkflowLike>();\n\n for (const wf of workflows) {\n const id = wf.definition.id;\n if (registry.has(id)) {\n throw new Error(`Duplicate workflow ID: '${id}'`);\n }\n registry.set(id, wf);\n }\n\n // Decorate fastify with workflow accessor\n if (!fastify.hasDecorator('workflows')) {\n fastify.decorate('workflows', registry);\n }\n if (!fastify.hasDecorator('getWorkflow')) {\n fastify.decorate('getWorkflow', (id: string) => registry.get(id) ?? null);\n }\n\n // Build auth preHandler if needed\n const authPreHandler = auth && typeof fastify.authenticate === 'function'\n ? [fastify.authenticate]\n : [];\n\n // Permission check helper\n const checkPerm = async (\n op: keyof NonNullable<StreamlinePluginOptions['permissions']>,\n request: unknown\n ): Promise<boolean> => {\n const check = perms?.[op];\n if (!check) return true;\n return check(request);\n };\n\n // Register routes per workflow\n for (const [id, wf] of registry) {\n const routePrefix = `${prefix}/${id}`;\n\n // POST /:workflowId/start — Start a new workflow run\n fastify.post(`${routePrefix}/start`, {\n preHandler: authPreHandler,\n }, async (request, reply) => {\n if (!(await checkPerm('start', request))) {\n return reply.status(403).send({ success: false, error: 'Forbidden' });\n }\n const { input, meta } = (request.body ?? {}) as { input?: unknown; meta?: unknown };\n const run = await wf.start(input, meta);\n\n // Bridge event to Arc's event bus\n if (bridgeEvents && fastify.events?.publish) {\n await fastify.events.publish(`workflow.${id}.started`, {\n runId: run._id,\n workflowId: id,\n status: run.status,\n });\n }\n\n return reply.status(201).send({ success: true, data: run });\n });\n\n // GET /:workflowId/runs/:runId — Get a workflow run\n fastify.get(`${routePrefix}/runs/:runId`, {\n preHandler: authPreHandler,\n }, async (request, reply) => {\n if (!(await checkPerm('get', request))) {\n return reply.status(403).send({ success: false, error: 'Forbidden' });\n }\n const { runId } = request.params as { runId: string };\n const run = await wf.get(runId);\n if (!run) {\n return reply.status(404).send({ success: false, error: 'Workflow run not found' });\n }\n return { success: true, data: run };\n });\n\n // POST /:workflowId/runs/:runId/resume — Resume a waiting workflow\n fastify.post(`${routePrefix}/runs/:runId/resume`, {\n preHandler: authPreHandler,\n }, async (request, reply) => {\n if (!(await checkPerm('resume', request))) {\n return reply.status(403).send({ success: false, error: 'Forbidden' });\n }\n const { runId } = request.params as { runId: string };\n const { payload } = (request.body ?? {}) as { payload?: unknown };\n const run = await wf.resume(runId, payload);\n\n if (bridgeEvents && fastify.events?.publish) {\n await fastify.events.publish(`workflow.${id}.resumed`, {\n runId: run._id,\n workflowId: id,\n status: run.status,\n });\n }\n\n return { success: true, data: run };\n });\n\n // POST /:workflowId/runs/:runId/cancel — Cancel a workflow run\n fastify.post(`${routePrefix}/runs/:runId/cancel`, {\n preHandler: authPreHandler,\n }, async (request, reply) => {\n if (!(await checkPerm('cancel', request))) {\n return reply.status(403).send({ success: false, error: 'Forbidden' });\n }\n const { runId } = request.params as { runId: string };\n const run = await wf.cancel(runId);\n\n if (bridgeEvents && fastify.events?.publish) {\n await fastify.events.publish(`workflow.${id}.cancelled`, {\n runId: run._id,\n workflowId: id,\n });\n }\n\n return { success: true, data: run };\n });\n\n // POST /:workflowId/runs/:runId/pause — Pause a running workflow (if supported)\n if (wf.engine.pause) {\n fastify.post(`${routePrefix}/runs/:runId/pause`, {\n preHandler: authPreHandler,\n }, async (request, reply) => {\n const { runId } = request.params as { runId: string };\n const run = await wf.engine.pause!(runId);\n return { success: true, data: run };\n });\n }\n\n // POST /:workflowId/runs/:runId/rewind — Rewind to a step (if supported)\n if (wf.engine.rewindTo) {\n fastify.post(`${routePrefix}/runs/:runId/rewind`, {\n preHandler: authPreHandler,\n }, async (request, reply) => {\n const { runId } = request.params as { runId: string };\n const { stepId } = (request.body ?? {}) as { stepId: string };\n if (!stepId) {\n return reply.status(400).send({ success: false, error: 'stepId is required' });\n }\n const run = await wf.engine.rewindTo!(runId, stepId);\n return { success: true, data: run };\n });\n }\n }\n\n // List all registered workflows\n fastify.get(prefix, {\n preHandler: authPreHandler,\n }, async () => {\n const list = Array.from(registry.entries()).map(([id, wf]) => ({\n id,\n name: wf.definition.name ?? id,\n steps: Object.keys(wf.definition.steps),\n }));\n return { success: true, data: list };\n });\n\n // Graceful shutdown\n fastify.addHook('onClose', async () => {\n for (const wf of registry.values()) {\n wf.shutdown?.();\n }\n });\n};\n\n/** Pluggable streamline integration for Arc */\nexport const streamlinePlugin: FastifyPluginAsync<StreamlinePluginOptions> = streamlinePluginImpl;\nexport default streamlinePlugin;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { FastifyPluginAsync } from "fastify";
|
|
2
|
+
|
|
3
|
+
//#region src/integrations/websocket.d.ts
|
|
4
|
+
interface WebSocketClient {
|
|
5
|
+
id: string;
|
|
6
|
+
socket: {
|
|
7
|
+
send(data: string): void;
|
|
8
|
+
close(): void;
|
|
9
|
+
readyState: number;
|
|
10
|
+
};
|
|
11
|
+
subscriptions: Set<string>;
|
|
12
|
+
userId?: string;
|
|
13
|
+
organizationId?: string;
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
interface WebSocketMessage {
|
|
17
|
+
type: string;
|
|
18
|
+
resource?: string;
|
|
19
|
+
channel?: string;
|
|
20
|
+
data?: unknown;
|
|
21
|
+
}
|
|
22
|
+
interface WebSocketPluginOptions {
|
|
23
|
+
/** WebSocket endpoint path (default: '/ws') */
|
|
24
|
+
path?: string;
|
|
25
|
+
/** Require authentication for WebSocket connections (default: true) */
|
|
26
|
+
auth?: boolean;
|
|
27
|
+
/** Resources to auto-broadcast CRUD events for */
|
|
28
|
+
resources?: string[];
|
|
29
|
+
/** Heartbeat interval in ms (default: 30000). Set 0 to disable. */
|
|
30
|
+
heartbeatInterval?: number;
|
|
31
|
+
/** Custom authentication function for WebSocket upgrade */
|
|
32
|
+
authenticate?: (request: unknown) => Promise<{
|
|
33
|
+
userId?: string;
|
|
34
|
+
organizationId?: string;
|
|
35
|
+
} | null>;
|
|
36
|
+
/** Max clients per resource subscription (default: 10000) */
|
|
37
|
+
maxClientsPerRoom?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Expose a stats endpoint at `{path}/stats`.
|
|
40
|
+
* - `false` (default): stats endpoint is not registered
|
|
41
|
+
* - `true`: registered without auth
|
|
42
|
+
* - `'authenticated'`: guarded by `fastify.authenticate` if available
|
|
43
|
+
*/
|
|
44
|
+
exposeStats?: boolean | "authenticated";
|
|
45
|
+
/**
|
|
46
|
+
* Authorize room subscriptions. Return true to allow, false to deny.
|
|
47
|
+
* Called before every subscribe. If not provided, all rooms are allowed.
|
|
48
|
+
*/
|
|
49
|
+
roomPolicy?: (client: WebSocketClient, room: string) => boolean | Promise<boolean>;
|
|
50
|
+
/** Maximum message size in bytes from client (default: 16384 = 16KB). Messages exceeding this are dropped. */
|
|
51
|
+
maxMessageBytes?: number;
|
|
52
|
+
/** Maximum subscriptions per client (default: 100). Prevents resource exhaustion. */
|
|
53
|
+
maxSubscriptionsPerClient?: number;
|
|
54
|
+
/** Custom message handler */
|
|
55
|
+
onMessage?: (client: WebSocketClient, message: WebSocketMessage) => void | Promise<void>;
|
|
56
|
+
/** Called when a client connects */
|
|
57
|
+
onConnect?: (client: WebSocketClient) => void | Promise<void>;
|
|
58
|
+
/** Called when a client disconnects */
|
|
59
|
+
onDisconnect?: (client: WebSocketClient) => void | Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
declare class RoomManager {
|
|
62
|
+
private rooms;
|
|
63
|
+
private clients;
|
|
64
|
+
private maxPerRoom;
|
|
65
|
+
constructor(maxPerRoom?: number);
|
|
66
|
+
addClient(client: WebSocketClient): void;
|
|
67
|
+
removeClient(clientId: string): void;
|
|
68
|
+
subscribe(clientId: string, room: string): boolean;
|
|
69
|
+
unsubscribe(clientId: string, room: string): void;
|
|
70
|
+
broadcast(room: string, message: string, excludeClientId?: string): void;
|
|
71
|
+
broadcastToOrg(organizationId: string, room: string, message: string): void;
|
|
72
|
+
getClient(clientId: string): WebSocketClient | undefined;
|
|
73
|
+
getStats(): {
|
|
74
|
+
clients: number;
|
|
75
|
+
rooms: number;
|
|
76
|
+
subscriptions: Record<string, number>;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Pluggable WebSocket integration for Arc */
|
|
80
|
+
declare const websocketPlugin: FastifyPluginAsync<WebSocketPluginOptions>;
|
|
81
|
+
//#endregion
|
|
82
|
+
export { RoomManager, WebSocketClient, WebSocketMessage, WebSocketPluginOptions, websocketPlugin as default, websocketPlugin };
|
|
83
|
+
//# sourceMappingURL=websocket.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.d.mts","names":[],"sources":["../../src/integrations/websocket.ts"],"mappings":";;;UAwCiB,eAAA;EACf,EAAA;EACA,MAAA;IAAU,IAAA,CAAK,IAAA;IAAqB,KAAA;IAAe,UAAA;EAAA;EACnD,aAAA,EAAe,GAAA;EACf,MAAA;EACA,cAAA;EACA,QAAA,GAAW,MAAA;AAAA;AAAA,UAGI,gBAAA;EACf,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;AAAA;AAAA,UAGe,sBAAA;EAYA;EAVf,IAAA;EAYA;EAVA,IAAA;EAsBA;EApBA,SAAA;EAqBE;EAnBF,iBAAA;EAqBe;EAnBf,YAAA,IACE,OAAA,cACG,OAAA;IAAU,MAAA;IAAiB,cAAA;EAAA;EAwB9B;EAtBF,iBAAA;EAuBE;;;;;;EAhBF,WAAA;EAqBwB;;;;EAhBxB,UAAA,IACE,MAAA,EAAQ,eAAA,EACR,IAAA,uBACa,OAAA;EAoBJ;EAlBX,eAAA;;EAEA,yBAAA;EA6G6B;EA3G7B,SAAA,IACE,MAAA,EAAQ,eAAA,EACR,OAAA,EAAS,gBAAA,YACC,OAAA;EA+GW;EA7GvB,SAAA,IAAa,MAAA,EAAQ,eAAA,YAA2B,OAAA;EAUxC;EARR,YAAA,IAAgB,MAAA,EAAQ,eAAA,YAA2B,OAAA;AAAA;AAAA,cAOxC,WAAA;EAAA,QACH,KAAA;EAAA,QACA,OAAA;EAAA,QACA,UAAA;cAEI,UAAA;EAIZ,SAAA,CAAU,MAAA,EAAQ,eAAA;EAIlB,YAAA,CAAa,QAAA;EAiBb,SAAA,CAAU,QAAA,UAAkB,IAAA;EAc5B,WAAA,CAAY,QAAA,UAAkB,IAAA;EAY9B,SAAA,CAAU,IAAA,UAAc,OAAA,UAAiB,eAAA;EAiBzC,cAAA,CAAe,cAAA,UAAwB,IAAA,UAAc,OAAA;EAoBrD,SAAA,CAAU,QAAA,WAAmB,eAAA;EAI7B,QAAA,CAAA;IACE,OAAA;IACA,KAAA;IACA,aAAA,EAAe,MAAA;EAAA;AAAA;;cAoUN,eAAA,EAGP,kBAAA,CAAmB,sBAAA"}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
|
|
3
|
+
//#region src/integrations/websocket.ts
|
|
4
|
+
var RoomManager = class {
|
|
5
|
+
rooms = /* @__PURE__ */ new Map();
|
|
6
|
+
clients = /* @__PURE__ */ new Map();
|
|
7
|
+
maxPerRoom;
|
|
8
|
+
constructor(maxPerRoom = 1e4) {
|
|
9
|
+
this.maxPerRoom = maxPerRoom;
|
|
10
|
+
}
|
|
11
|
+
addClient(client) {
|
|
12
|
+
this.clients.set(client.id, client);
|
|
13
|
+
}
|
|
14
|
+
removeClient(clientId) {
|
|
15
|
+
const client = this.clients.get(clientId);
|
|
16
|
+
if (!client) return;
|
|
17
|
+
for (const room of client.subscriptions) {
|
|
18
|
+
const members = this.rooms.get(room);
|
|
19
|
+
if (members) {
|
|
20
|
+
members.delete(clientId);
|
|
21
|
+
if (members.size === 0) this.rooms.delete(room);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
client.subscriptions.clear();
|
|
25
|
+
this.clients.delete(clientId);
|
|
26
|
+
}
|
|
27
|
+
subscribe(clientId, room) {
|
|
28
|
+
const client = this.clients.get(clientId);
|
|
29
|
+
if (!client) return false;
|
|
30
|
+
const members = this.rooms.get(room);
|
|
31
|
+
if (members && members.size >= this.maxPerRoom) return false;
|
|
32
|
+
if (!this.rooms.has(room)) this.rooms.set(room, /* @__PURE__ */ new Set());
|
|
33
|
+
this.rooms.get(room).add(clientId);
|
|
34
|
+
client.subscriptions.add(room);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
unsubscribe(clientId, room) {
|
|
38
|
+
const client = this.clients.get(clientId);
|
|
39
|
+
if (!client) return;
|
|
40
|
+
const members = this.rooms.get(room);
|
|
41
|
+
if (members) {
|
|
42
|
+
members.delete(clientId);
|
|
43
|
+
if (members.size === 0) this.rooms.delete(room);
|
|
44
|
+
}
|
|
45
|
+
client.subscriptions.delete(room);
|
|
46
|
+
}
|
|
47
|
+
broadcast(room, message, excludeClientId) {
|
|
48
|
+
const members = this.rooms.get(room);
|
|
49
|
+
if (!members) return;
|
|
50
|
+
for (const clientId of members) {
|
|
51
|
+
if (clientId === excludeClientId) continue;
|
|
52
|
+
const client = this.clients.get(clientId);
|
|
53
|
+
if (client && client.socket.readyState === 1) try {
|
|
54
|
+
client.socket.send(message);
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
broadcastToOrg(organizationId, room, message) {
|
|
59
|
+
const members = this.rooms.get(room);
|
|
60
|
+
if (!members) return;
|
|
61
|
+
for (const clientId of members) {
|
|
62
|
+
const client = this.clients.get(clientId);
|
|
63
|
+
if (client && client.organizationId === organizationId && client.socket.readyState === 1) try {
|
|
64
|
+
client.socket.send(message);
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
getClient(clientId) {
|
|
69
|
+
return this.clients.get(clientId);
|
|
70
|
+
}
|
|
71
|
+
getStats() {
|
|
72
|
+
const subscriptions = {};
|
|
73
|
+
for (const [room, members] of this.rooms) subscriptions[room] = members.size;
|
|
74
|
+
return {
|
|
75
|
+
clients: this.clients.size,
|
|
76
|
+
rooms: this.rooms.size,
|
|
77
|
+
subscriptions
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const websocketPluginImpl = async (fastify, options) => {
|
|
82
|
+
let clientCounter = 0;
|
|
83
|
+
const { path = "/ws", auth = true, resources = [], heartbeatInterval = 3e4, authenticate: customAuth, maxClientsPerRoom = 1e4, roomPolicy, maxMessageBytes = 16384, maxSubscriptionsPerClient = 100, exposeStats = false, onMessage, onConnect, onDisconnect } = options;
|
|
84
|
+
if (auth && !customAuth && !fastify.hasDecorator("authenticate")) throw new Error("[arc-websocket] auth is true but fastify.authenticate is not registered. Register an auth plugin before WebSocket, provide a custom authenticate function, or set auth: false.");
|
|
85
|
+
const rooms = new RoomManager(maxClientsPerRoom);
|
|
86
|
+
if (!fastify.hasDecorator("ws")) fastify.decorate("ws", {
|
|
87
|
+
rooms,
|
|
88
|
+
broadcast: (room, data) => {
|
|
89
|
+
rooms.broadcast(room, JSON.stringify({
|
|
90
|
+
type: "broadcast",
|
|
91
|
+
channel: room,
|
|
92
|
+
data
|
|
93
|
+
}));
|
|
94
|
+
},
|
|
95
|
+
broadcastToOrg: (orgId, room, data) => {
|
|
96
|
+
rooms.broadcastToOrg(orgId, room, JSON.stringify({
|
|
97
|
+
type: "broadcast",
|
|
98
|
+
channel: room,
|
|
99
|
+
data
|
|
100
|
+
}));
|
|
101
|
+
},
|
|
102
|
+
getStats: () => rooms.getStats()
|
|
103
|
+
});
|
|
104
|
+
const eventUnsubscribers = [];
|
|
105
|
+
if (resources.length > 0 && fastify.events?.subscribe) for (const resourceName of resources) for (const op of [
|
|
106
|
+
"created",
|
|
107
|
+
"updated",
|
|
108
|
+
"deleted"
|
|
109
|
+
]) {
|
|
110
|
+
const unsub = await fastify.events.subscribe(`${resourceName}.${op}`, async (event) => {
|
|
111
|
+
const room = resourceName;
|
|
112
|
+
const payload = JSON.stringify({
|
|
113
|
+
type: `${resourceName}.${op}`,
|
|
114
|
+
data: event.payload,
|
|
115
|
+
meta: {
|
|
116
|
+
timestamp: event.meta?.timestamp,
|
|
117
|
+
userId: event.meta?.userId,
|
|
118
|
+
organizationId: event.meta?.organizationId
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
if (event.meta?.organizationId) rooms.broadcastToOrg(event.meta.organizationId, room, payload);
|
|
122
|
+
else rooms.broadcast(room, payload);
|
|
123
|
+
});
|
|
124
|
+
eventUnsubscribers.push(unsub);
|
|
125
|
+
}
|
|
126
|
+
fastify.get(path, { websocket: true }, async (socket, request) => {
|
|
127
|
+
const clientId = `ws_${++clientCounter}_${Date.now()}`;
|
|
128
|
+
let userId;
|
|
129
|
+
let organizationId;
|
|
130
|
+
if (auth) if (customAuth) {
|
|
131
|
+
const result = await customAuth(request);
|
|
132
|
+
if (!result) {
|
|
133
|
+
socket.close(4001, "Unauthorized");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
userId = result.userId;
|
|
137
|
+
organizationId = result.organizationId;
|
|
138
|
+
} else {
|
|
139
|
+
if (fastify.authenticate) try {
|
|
140
|
+
let rejected = false;
|
|
141
|
+
const fakeReply = {
|
|
142
|
+
code(_statusCode) {
|
|
143
|
+
rejected = true;
|
|
144
|
+
return fakeReply;
|
|
145
|
+
},
|
|
146
|
+
send() {
|
|
147
|
+
return fakeReply;
|
|
148
|
+
},
|
|
149
|
+
sent: false
|
|
150
|
+
};
|
|
151
|
+
await fastify.authenticate(request, fakeReply);
|
|
152
|
+
if (rejected) {
|
|
153
|
+
socket.close(4001, "Unauthorized");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
socket.close(4001, "Unauthorized");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (request.user) {
|
|
161
|
+
userId = request.user.id ?? request.user.sub;
|
|
162
|
+
organizationId = request.scope?.organizationId;
|
|
163
|
+
} else {
|
|
164
|
+
socket.close(4001, "Unauthorized");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const client = {
|
|
169
|
+
id: clientId,
|
|
170
|
+
socket,
|
|
171
|
+
subscriptions: /* @__PURE__ */ new Set(),
|
|
172
|
+
userId,
|
|
173
|
+
organizationId
|
|
174
|
+
};
|
|
175
|
+
rooms.addClient(client);
|
|
176
|
+
await onConnect?.(client);
|
|
177
|
+
socket.send(JSON.stringify({
|
|
178
|
+
type: "connected",
|
|
179
|
+
clientId,
|
|
180
|
+
resources
|
|
181
|
+
}));
|
|
182
|
+
let heartbeatTimer;
|
|
183
|
+
if (heartbeatInterval > 0) heartbeatTimer = setInterval(() => {
|
|
184
|
+
if (socket.readyState === 1) socket.send(JSON.stringify({
|
|
185
|
+
type: "ping",
|
|
186
|
+
timestamp: Date.now()
|
|
187
|
+
}));
|
|
188
|
+
}, heartbeatInterval);
|
|
189
|
+
socket.on("message", async (raw) => {
|
|
190
|
+
if ((typeof raw === "string" ? Buffer.byteLength(raw) : raw.length) > maxMessageBytes) {
|
|
191
|
+
socket.send(JSON.stringify({
|
|
192
|
+
type: "error",
|
|
193
|
+
error: "Message too large"
|
|
194
|
+
}));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const msg = JSON.parse(typeof raw === "string" ? raw : raw.toString());
|
|
199
|
+
switch (msg.type) {
|
|
200
|
+
case "subscribe": {
|
|
201
|
+
const room = msg.resource ?? msg.channel;
|
|
202
|
+
if (room) {
|
|
203
|
+
if (client.subscriptions.size >= maxSubscriptionsPerClient) {
|
|
204
|
+
socket.send(JSON.stringify({
|
|
205
|
+
type: "error",
|
|
206
|
+
channel: room,
|
|
207
|
+
error: "Subscription limit reached"
|
|
208
|
+
}));
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
if (roomPolicy) {
|
|
212
|
+
if (!await roomPolicy(client, room)) {
|
|
213
|
+
socket.send(JSON.stringify({
|
|
214
|
+
type: "error",
|
|
215
|
+
channel: room,
|
|
216
|
+
error: "Subscription denied"
|
|
217
|
+
}));
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const ok = rooms.subscribe(clientId, room);
|
|
222
|
+
socket.send(JSON.stringify({
|
|
223
|
+
type: ok ? "subscribed" : "error",
|
|
224
|
+
channel: room,
|
|
225
|
+
...ok ? {} : { error: "Room at capacity" }
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case "unsubscribe": {
|
|
231
|
+
const room = msg.resource ?? msg.channel;
|
|
232
|
+
if (room) {
|
|
233
|
+
rooms.unsubscribe(clientId, room);
|
|
234
|
+
socket.send(JSON.stringify({
|
|
235
|
+
type: "unsubscribed",
|
|
236
|
+
channel: room
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case "pong": break;
|
|
242
|
+
default:
|
|
243
|
+
await onMessage?.(client, msg);
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
socket.send(JSON.stringify({
|
|
248
|
+
type: "error",
|
|
249
|
+
error: "Invalid message format"
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
socket.on("close", async () => {
|
|
254
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
255
|
+
await onDisconnect?.(client);
|
|
256
|
+
rooms.removeClient(clientId);
|
|
257
|
+
});
|
|
258
|
+
socket.on("error", () => {
|
|
259
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
260
|
+
rooms.removeClient(clientId);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
if (exposeStats === true) fastify.get(`${path}/stats`, async () => {
|
|
264
|
+
return {
|
|
265
|
+
success: true,
|
|
266
|
+
data: rooms.getStats()
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
else if (exposeStats === "authenticated") if (fastify.hasDecorator("authenticate")) fastify.get(`${path}/stats`, { preHandler: fastify.authenticate }, async () => {
|
|
270
|
+
return {
|
|
271
|
+
success: true,
|
|
272
|
+
data: rooms.getStats()
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
else fastify.log.warn("arc-websocket: exposeStats is \"authenticated\" but fastify.authenticate is not registered — stats endpoint skipped");
|
|
276
|
+
fastify.addHook("onClose", async () => {
|
|
277
|
+
for (const unsub of eventUnsubscribers) unsub();
|
|
278
|
+
eventUnsubscribers.length = 0;
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
/** Pluggable WebSocket integration for Arc */
|
|
282
|
+
const websocketPlugin = fp(websocketPluginImpl, {
|
|
283
|
+
name: "arc-websocket",
|
|
284
|
+
fastify: "5.x"
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
//#endregion
|
|
288
|
+
export { RoomManager, websocketPlugin as default, websocketPlugin };
|
|
289
|
+
//# sourceMappingURL=websocket.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.mjs","names":[],"sources":["../../src/integrations/websocket.ts"],"sourcesContent":["/**\n * @classytic/arc — WebSocket Integration\n *\n * Pluggable adapter that wires @fastify/websocket into Arc's resource system.\n * Provides room-based subscriptions, auto-broadcasts resource CRUD events,\n * and respects Arc's auth/org scoping.\n *\n * This is a SEPARATE subpath import — only loaded when explicitly used:\n * import { websocketPlugin } from '@classytic/arc/integrations/websocket';\n *\n * Requires: @fastify/websocket (peer dependency)\n *\n * NOTE: WebSocket requires persistent connections. This does NOT work on\n * serverless platforms (Lambda, Vercel). Only use on persistent runtimes\n * (Docker, VPS, K8s, Cloud Run with min-instances > 0).\n *\n * @example\n * ```typescript\n * import { websocketPlugin } from '@classytic/arc/integrations/websocket';\n *\n * await fastify.register(websocketPlugin, {\n * path: '/ws',\n * auth: true,\n * resources: ['product', 'order'], // Auto-broadcast CRUD events\n * heartbeatInterval: 30000,\n * });\n *\n * // Client connects to ws://localhost:3000/ws\n * // Server pushes: { type: 'product.created', data: { ... } }\n * // Client sends: { type: 'subscribe', resource: 'product' }\n * // Client sends: { type: 'unsubscribe', resource: 'product' }\n * ```\n */\nimport type { FastifyInstance, FastifyPluginAsync } from \"fastify\";\nimport fp from \"fastify-plugin\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface WebSocketClient {\n id: string;\n socket: { send(data: string): void; close(): void; readyState: number };\n subscriptions: Set<string>;\n userId?: string;\n organizationId?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface WebSocketMessage {\n type: string;\n resource?: string;\n channel?: string;\n data?: unknown;\n}\n\nexport interface WebSocketPluginOptions {\n /** WebSocket endpoint path (default: '/ws') */\n path?: string;\n /** Require authentication for WebSocket connections (default: true) */\n auth?: boolean;\n /** Resources to auto-broadcast CRUD events for */\n resources?: string[];\n /** Heartbeat interval in ms (default: 30000). Set 0 to disable. */\n heartbeatInterval?: number;\n /** Custom authentication function for WebSocket upgrade */\n authenticate?: (\n request: unknown,\n ) => Promise<{ userId?: string; organizationId?: string } | null>;\n /** Max clients per resource subscription (default: 10000) */\n maxClientsPerRoom?: number;\n /**\n * Expose a stats endpoint at `{path}/stats`.\n * - `false` (default): stats endpoint is not registered\n * - `true`: registered without auth\n * - `'authenticated'`: guarded by `fastify.authenticate` if available\n */\n exposeStats?: boolean | \"authenticated\";\n /**\n * Authorize room subscriptions. Return true to allow, false to deny.\n * Called before every subscribe. If not provided, all rooms are allowed.\n */\n roomPolicy?: (\n client: WebSocketClient,\n room: string,\n ) => boolean | Promise<boolean>;\n /** Maximum message size in bytes from client (default: 16384 = 16KB). Messages exceeding this are dropped. */\n maxMessageBytes?: number;\n /** Maximum subscriptions per client (default: 100). Prevents resource exhaustion. */\n maxSubscriptionsPerClient?: number;\n /** Custom message handler */\n onMessage?: (\n client: WebSocketClient,\n message: WebSocketMessage,\n ) => void | Promise<void>;\n /** Called when a client connects */\n onConnect?: (client: WebSocketClient) => void | Promise<void>;\n /** Called when a client disconnects */\n onDisconnect?: (client: WebSocketClient) => void | Promise<void>;\n}\n\n// ============================================================================\n// Room Manager — manages subscriptions efficiently\n// ============================================================================\n\nexport class RoomManager {\n private rooms = new Map<string, Set<string>>(); // room → clientIds\n private clients = new Map<string, WebSocketClient>(); // clientId → client\n private maxPerRoom: number;\n\n constructor(maxPerRoom = 10000) {\n this.maxPerRoom = maxPerRoom;\n }\n\n addClient(client: WebSocketClient): void {\n this.clients.set(client.id, client);\n }\n\n removeClient(clientId: string): void {\n const client = this.clients.get(clientId);\n if (!client) return;\n\n // Remove from all rooms\n for (const room of client.subscriptions) {\n const members = this.rooms.get(room);\n if (members) {\n members.delete(clientId);\n if (members.size === 0) this.rooms.delete(room);\n }\n }\n\n client.subscriptions.clear();\n this.clients.delete(clientId);\n }\n\n subscribe(clientId: string, room: string): boolean {\n const client = this.clients.get(clientId);\n if (!client) return false;\n\n // Check room capacity\n const members = this.rooms.get(room);\n if (members && members.size >= this.maxPerRoom) return false;\n\n if (!this.rooms.has(room)) this.rooms.set(room, new Set());\n this.rooms.get(room)!.add(clientId);\n client.subscriptions.add(room);\n return true;\n }\n\n unsubscribe(clientId: string, room: string): void {\n const client = this.clients.get(clientId);\n if (!client) return;\n\n const members = this.rooms.get(room);\n if (members) {\n members.delete(clientId);\n if (members.size === 0) this.rooms.delete(room);\n }\n client.subscriptions.delete(room);\n }\n\n broadcast(room: string, message: string, excludeClientId?: string): void {\n const members = this.rooms.get(room);\n if (!members) return;\n\n for (const clientId of members) {\n if (clientId === excludeClientId) continue;\n const client = this.clients.get(clientId);\n if (client && client.socket.readyState === 1) {\n try {\n client.socket.send(message);\n } catch {\n // Client disconnected, will be cleaned up\n }\n }\n }\n }\n\n broadcastToOrg(organizationId: string, room: string, message: string): void {\n const members = this.rooms.get(room);\n if (!members) return;\n\n for (const clientId of members) {\n const client = this.clients.get(clientId);\n if (\n client &&\n client.organizationId === organizationId &&\n client.socket.readyState === 1\n ) {\n try {\n client.socket.send(message);\n } catch {\n // Client disconnected\n }\n }\n }\n }\n\n getClient(clientId: string): WebSocketClient | undefined {\n return this.clients.get(clientId);\n }\n\n getStats(): {\n clients: number;\n rooms: number;\n subscriptions: Record<string, number>;\n } {\n const subscriptions: Record<string, number> = {};\n for (const [room, members] of this.rooms) {\n subscriptions[room] = members.size;\n }\n return {\n clients: this.clients.size,\n rooms: this.rooms.size,\n subscriptions,\n };\n }\n}\n\n// ============================================================================\n// Plugin Implementation\n// ============================================================================\n\nconst websocketPluginImpl: FastifyPluginAsync<WebSocketPluginOptions> = async (\n fastify: FastifyInstance,\n options: WebSocketPluginOptions,\n) => {\n // Instance-scoped counter — no global leak across test runs or multiple app instances\n let clientCounter = 0;\n const {\n path = \"/ws\",\n auth = true,\n resources = [],\n heartbeatInterval = 30000,\n authenticate: customAuth,\n maxClientsPerRoom = 10000,\n roomPolicy,\n maxMessageBytes = 16384,\n maxSubscriptionsPerClient = 100,\n exposeStats = false,\n onMessage,\n onConnect,\n onDisconnect,\n } = options;\n\n // Fail-closed: throw early if auth required but no authenticator available\n if (auth && !customAuth && !fastify.hasDecorator(\"authenticate\")) {\n throw new Error(\n \"[arc-websocket] auth is true but fastify.authenticate is not registered. \" +\n \"Register an auth plugin before WebSocket, provide a custom authenticate function, or set auth: false.\",\n );\n }\n\n const rooms = new RoomManager(maxClientsPerRoom);\n\n // Decorate fastify with room manager for external access\n if (!fastify.hasDecorator(\"ws\")) {\n fastify.decorate(\"ws\", {\n rooms,\n broadcast: (room: string, data: unknown) => {\n rooms.broadcast(\n room,\n JSON.stringify({ type: \"broadcast\", channel: room, data }),\n );\n },\n broadcastToOrg: (orgId: string, room: string, data: unknown) => {\n rooms.broadcastToOrg(\n orgId,\n room,\n JSON.stringify({ type: \"broadcast\", channel: room, data }),\n );\n },\n getStats: () => rooms.getStats(),\n });\n }\n\n // Wire into Arc's event bus for auto-broadcasting resource events\n // Track unsubscribe handles so we can clean up on server close\n const eventUnsubscribers: Array<() => void> = [];\n\n if (resources.length > 0 && fastify.events?.subscribe) {\n for (const resourceName of resources) {\n for (const op of [\"created\", \"updated\", \"deleted\"] as const) {\n const unsub = await fastify.events.subscribe(\n `${resourceName}.${op}`,\n async (event: any) => {\n const room = resourceName;\n const payload = JSON.stringify({\n type: `${resourceName}.${op}`,\n data: event.payload,\n meta: {\n timestamp: event.meta?.timestamp,\n userId: event.meta?.userId,\n organizationId: event.meta?.organizationId,\n },\n });\n\n // If org-scoped, only broadcast to clients in same org\n if (event.meta?.organizationId) {\n rooms.broadcastToOrg(event.meta.organizationId, room, payload);\n } else {\n rooms.broadcast(room, payload);\n }\n },\n );\n eventUnsubscribers.push(unsub);\n }\n }\n }\n\n // Register WebSocket route\n // Requires @fastify/websocket to be registered beforehand\n fastify.get(\n path,\n { websocket: true } as any,\n async (socket: any, request: any) => {\n const clientId = `ws_${++clientCounter}_${Date.now()}`;\n\n // Authentication\n let userId: string | undefined;\n let organizationId: string | undefined;\n\n if (auth) {\n if (customAuth) {\n const result = await customAuth(request);\n if (!result) {\n socket.close(4001, \"Unauthorized\");\n return;\n }\n userId = result.userId;\n organizationId = result.organizationId;\n } else {\n // Run fastify.authenticate to parse token and populate request.user\n // during the WebSocket handshake. Without this, request.user is never\n // set and all authenticated WS connections are rejected.\n if (fastify.authenticate) {\n try {\n // Create a minimal reply-like object for authenticate()\n // that captures the status code without sending a real HTTP response\n let rejected = false;\n const fakeReply = {\n code(_statusCode: number) { rejected = true; return fakeReply; },\n send() { return fakeReply; },\n sent: false,\n };\n await (fastify.authenticate as any)(request, fakeReply);\n if (rejected) {\n socket.close(4001, \"Unauthorized\");\n return;\n }\n } catch {\n socket.close(4001, \"Unauthorized\");\n return;\n }\n }\n\n if (request.user) {\n userId = (request.user as any).id ?? (request.user as any).sub;\n organizationId = (request.scope as any)?.organizationId;\n } else {\n socket.close(4001, \"Unauthorized\");\n return;\n }\n }\n }\n\n const client: WebSocketClient = {\n id: clientId,\n socket,\n subscriptions: new Set(),\n userId,\n organizationId,\n };\n\n rooms.addClient(client);\n await onConnect?.(client);\n\n // Send connection confirmation\n socket.send(\n JSON.stringify({\n type: \"connected\",\n clientId,\n resources: resources,\n }),\n );\n\n // Heartbeat\n let heartbeatTimer: ReturnType<typeof setInterval> | undefined;\n if (heartbeatInterval > 0) {\n heartbeatTimer = setInterval(() => {\n if (socket.readyState === 1) {\n socket.send(\n JSON.stringify({ type: \"ping\", timestamp: Date.now() }),\n );\n }\n }, heartbeatInterval);\n }\n\n // Handle incoming messages\n socket.on(\"message\", async (raw: Buffer | string) => {\n // Message size cap — drop oversized messages\n const rawSize = typeof raw === \"string\" ? Buffer.byteLength(raw) : raw.length;\n if (rawSize > maxMessageBytes) {\n socket.send(\n JSON.stringify({ type: \"error\", error: \"Message too large\" }),\n );\n return;\n }\n\n try {\n const msg: WebSocketMessage = JSON.parse(\n typeof raw === \"string\" ? raw : raw.toString(),\n );\n\n switch (msg.type) {\n case \"subscribe\": {\n const room = msg.resource ?? msg.channel;\n if (room) {\n // Subscription limit per client\n if (client.subscriptions.size >= maxSubscriptionsPerClient) {\n socket.send(\n JSON.stringify({\n type: \"error\",\n channel: room,\n error: \"Subscription limit reached\",\n }),\n );\n break;\n }\n\n // Room authorization policy\n if (roomPolicy) {\n const allowed = await roomPolicy(client, room);\n if (!allowed) {\n socket.send(\n JSON.stringify({\n type: \"error\",\n channel: room,\n error: \"Subscription denied\",\n }),\n );\n break;\n }\n }\n\n const ok = rooms.subscribe(clientId, room);\n socket.send(\n JSON.stringify({\n type: ok ? \"subscribed\" : \"error\",\n channel: room,\n ...(ok ? {} : { error: \"Room at capacity\" }),\n }),\n );\n }\n break;\n }\n\n case \"unsubscribe\": {\n const room = msg.resource ?? msg.channel;\n if (room) {\n rooms.unsubscribe(clientId, room);\n socket.send(\n JSON.stringify({ type: \"unsubscribed\", channel: room }),\n );\n }\n break;\n }\n\n case \"pong\":\n // Heartbeat response, ignore\n break;\n\n default:\n // Forward to custom handler\n await onMessage?.(client, msg);\n break;\n }\n } catch {\n socket.send(\n JSON.stringify({ type: \"error\", error: \"Invalid message format\" }),\n );\n }\n });\n\n // Cleanup on disconnect\n socket.on(\"close\", async () => {\n if (heartbeatTimer) clearInterval(heartbeatTimer);\n await onDisconnect?.(client);\n rooms.removeClient(clientId);\n });\n\n socket.on(\"error\", () => {\n if (heartbeatTimer) clearInterval(heartbeatTimer);\n rooms.removeClient(clientId);\n });\n },\n );\n\n // Stats endpoint (opt-in)\n if (exposeStats === true) {\n fastify.get(`${path}/stats`, async () => {\n return { success: true, data: rooms.getStats() };\n });\n } else if (exposeStats === \"authenticated\") {\n if (fastify.hasDecorator(\"authenticate\")) {\n fastify.get(\n `${path}/stats`,\n { preHandler: fastify.authenticate } as any,\n async () => {\n return { success: true, data: rooms.getStats() };\n },\n );\n } else {\n fastify.log.warn(\n 'arc-websocket: exposeStats is \"authenticated\" but fastify.authenticate is not registered — stats endpoint skipped',\n );\n }\n }\n\n // Cleanup on server close — unsubscribe event handlers to prevent leaks\n fastify.addHook(\"onClose\", async () => {\n for (const unsub of eventUnsubscribers) {\n unsub();\n }\n eventUnsubscribers.length = 0;\n });\n};\n\n/** Pluggable WebSocket integration for Arc */\nexport const websocketPlugin = fp(websocketPluginImpl, {\n name: \"arc-websocket\",\n fastify: \"5.x\",\n}) as FastifyPluginAsync<WebSocketPluginOptions>;\nexport default websocketPlugin;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//#region src/idempotency/stores/interface.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Idempotency Store Interface
|
|
4
|
+
*
|
|
5
|
+
* Defines the contract for idempotency key storage backends.
|
|
6
|
+
* Implement this interface for custom stores (Redis, DynamoDB, etc.)
|
|
7
|
+
*/
|
|
8
|
+
interface IdempotencyResult {
|
|
9
|
+
/** The idempotency key */
|
|
10
|
+
key: string;
|
|
11
|
+
/** HTTP status code of the cached response */
|
|
12
|
+
statusCode: number;
|
|
13
|
+
/** Response headers to replay */
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
/** Response body */
|
|
16
|
+
body: unknown;
|
|
17
|
+
/** When this entry was created */
|
|
18
|
+
createdAt: Date;
|
|
19
|
+
/** When this entry expires */
|
|
20
|
+
expiresAt: Date;
|
|
21
|
+
}
|
|
22
|
+
interface IdempotencyLock {
|
|
23
|
+
/** The idempotency key being locked */
|
|
24
|
+
key: string;
|
|
25
|
+
/** Request ID that holds the lock */
|
|
26
|
+
requestId: string;
|
|
27
|
+
/** When the lock was acquired */
|
|
28
|
+
lockedAt: Date;
|
|
29
|
+
/** When the lock expires (auto-release) */
|
|
30
|
+
expiresAt: Date;
|
|
31
|
+
}
|
|
32
|
+
interface IdempotencyStore {
|
|
33
|
+
/** Store name for logging */
|
|
34
|
+
readonly name: string;
|
|
35
|
+
/**
|
|
36
|
+
* Get a cached result for an idempotency key.
|
|
37
|
+
* Returns undefined if not found or expired.
|
|
38
|
+
*/
|
|
39
|
+
get(key: string): Promise<IdempotencyResult | undefined>;
|
|
40
|
+
/**
|
|
41
|
+
* Store a result for an idempotency key.
|
|
42
|
+
* TTL is handled by the store implementation.
|
|
43
|
+
*/
|
|
44
|
+
set(key: string, result: Omit<IdempotencyResult, 'key'>): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Try to acquire a lock for processing a key.
|
|
47
|
+
* Returns true if lock acquired, false if already locked.
|
|
48
|
+
*/
|
|
49
|
+
tryLock(key: string, requestId: string, ttlMs: number): Promise<boolean>;
|
|
50
|
+
/** Release a lock after processing complete */
|
|
51
|
+
unlock(key: string, requestId: string): Promise<void>;
|
|
52
|
+
/** Check if a key is currently locked */
|
|
53
|
+
isLocked(key: string): Promise<boolean>;
|
|
54
|
+
/** Delete a cached result by exact key */
|
|
55
|
+
delete(key: string): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Delete all cached results whose key starts with the given prefix.
|
|
58
|
+
* Used by invalidate() to clear entries by raw idempotency key
|
|
59
|
+
* regardless of fingerprint.
|
|
60
|
+
* Returns the number of entries deleted.
|
|
61
|
+
*/
|
|
62
|
+
deleteByPrefix(prefix: string): Promise<number>;
|
|
63
|
+
/**
|
|
64
|
+
* Find the first cached result whose key starts with the given prefix.
|
|
65
|
+
* Used by has() to check if any entry exists for a raw idempotency key.
|
|
66
|
+
* Returns undefined if no matching entry found.
|
|
67
|
+
*/
|
|
68
|
+
findByPrefix(prefix: string): Promise<IdempotencyResult | undefined>;
|
|
69
|
+
/** Close the store (cleanup connections) */
|
|
70
|
+
close?(): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Helper to create a result object
|
|
74
|
+
*/
|
|
75
|
+
declare function createIdempotencyResult(statusCode: number, body: unknown, headers: Record<string, string>, ttlMs: number): Omit<IdempotencyResult, 'key'>;
|
|
76
|
+
//#endregion
|
|
77
|
+
export { createIdempotencyResult as i, IdempotencyResult as n, IdempotencyStore as r, IdempotencyLock as t };
|
|
78
|
+
//# sourceMappingURL=interface-B01JvPVc.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interface-B01JvPVc.d.mts","names":[],"sources":["../src/idempotency/stores/interface.ts"],"mappings":";;AAOA;;;;;UAAiB,iBAAA;EAYA;EAVf,GAAA;EAAA;EAEA,UAAA;EAEA;EAAA,OAAA,EAAS,MAAA;EAET;EAAA,IAAA;EAEW;EAAX,SAAA,EAAW,IAAA;EAEA;EAAX,SAAA,EAAW,IAAA;AAAA;AAAA,UAGI,eAAA;EAAe;EAE9B,GAAA;EAMe;EAJf,SAAA;EAAA;EAEA,QAAA,EAAU,IAAA;EAAA;EAEV,SAAA,EAAW,IAAA;AAAA;AAAA,UAGI,gBAAA;EAHA;EAAA,SAKN,IAAA;EAFsB;;;;EAQ/B,GAAA,CAAI,GAAA,WAAc,OAAA,CAAQ,iBAAA;EAMD;;;;EAAzB,GAAA,CAAI,GAAA,UAAa,MAAA,EAAQ,IAAA,CAAK,iBAAA,WAA4B,OAAA;EAerC;;;;EATrB,OAAA,CAAQ,GAAA,UAAa,SAAA,UAAmB,KAAA,WAAgB,OAAA;EA2BvC;EAxBjB,MAAA,CAAO,GAAA,UAAa,SAAA,WAAoB,OAAA;EArB/B;EAwBT,QAAA,CAAS,GAAA,WAAc,OAAA;EAlBnB;EAqBJ,MAAA,CAAO,GAAA,WAAc,OAAA;EArBK;;;;;;EA6B1B,cAAA,CAAe,MAAA,WAAiB,OAAA;EAjBhC;;;;;EAwBA,YAAA,CAAa,MAAA,WAAiB,OAAA,CAAQ,iBAAA;EArB/B;EAwBP,KAAA,KAAU,OAAA;AAAA;;;;iBAMI,uBAAA,CACd,UAAA,UACA,IAAA,WACA,OAAA,EAAS,MAAA,kBACT,KAAA,WACC,IAAA,CAAK,iBAAA"}
|