@classytic/arc 2.17.1 → 2.18.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/dist/{BaseController-CgHomZ4d.mjs → BaseController-DPwe89qs.mjs} +1 -1
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/context/index.d.mts +54 -1
- package/dist/context/index.mjs +65 -2
- package/dist/core/index.mjs +3 -3
- package/dist/{core-D8tpQ4ZL.mjs → core-B41eZxyM.mjs} +8 -5
- package/dist/{createActionRouter-BHUMb1-y.mjs → createActionRouter-CRaIwB5S.mjs} +2 -2
- package/dist/{createAggregationRouter-BYVvhfc5.mjs → createAggregationRouter-n3O6WdLO.mjs} +1 -1
- package/dist/{createApp-CzCEEsEm.mjs → createApp-D56t2Dsj.mjs} +7 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{eventPlugin-CnJCUyM0.mjs → eventPlugin-Dxp9JlKK.mjs} +1 -1
- package/dist/events/index.d.mts +48 -0
- package/dist/events/index.mjs +27 -5
- package/dist/factory/index.mjs +1 -1
- package/dist/index.mjs +4 -4
- package/dist/{init-DYzfniuX.mjs → init-DQ_rY1J7.mjs} +13 -13
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/mcp/index.d.mts +42 -2
- package/dist/integrations/mcp/index.mjs +102 -2
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/websocket-pushref-redis.d.mts +71 -0
- package/dist/integrations/websocket-pushref-redis.mjs +205 -0
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +3 -2
- package/dist/integrations/websocket.mjs +1142 -23
- package/dist/{mcpPlugin-C6FViZXw.mjs → mcpPlugin-BrepZ0cV.mjs} +3 -3
- package/dist/{openapi-DKDs9NfD.mjs → openapi-CLZx9bwS.mjs} +1 -1
- package/dist/plugins/index.d.mts +15 -1
- package/dist/plugins/index.mjs +44 -3
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +1 -1
- package/dist/pushref-registry-Bq_-EVsC.d.mts +435 -0
- package/dist/{requestContext-SSaaTgW8.mjs → requestContext-DDLS3NVU.mjs} +26 -1
- package/dist/{routerShared-nCyJGOKX.mjs → routerShared-Dl0A77Ba.mjs} +1 -1
- package/dist/{schemaIR-DAOvBWgT.mjs → schemaIR-Bk6Tns82.mjs} +17 -5
- package/dist/testing/index.mjs +1 -1
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/storage.d.mts +1 -1
- package/dist/websocket-BZMUP7Gr.d.mts +456 -0
- package/package.json +12 -1
- package/dist/websocket-BJ0w-3Dt.d.mts +0 -186
- /package/dist/{storage-HcAgP5Ni.d.mts → storage-BFmamN5m.d.mts} +0 -0
- /package/dist/{tracing-bRqqUxFO.d.mts → tracing-BYQ3hsbM.d.mts} +0 -0
- /package/dist/{types-CoMjNmkF.d.mts → types-D3HV0r5L.d.mts} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as defaultCrudDescription, c as mcpHandlerAdapter, d as toCallToolResult, f as toCallToolSuccess, h as resolveToolCollisions, i as fieldRulesToZod, l as permissionDeniedResult, m as createMcpServer, n as mcpPlugin, o as resolveCrudDescription, p as buildRequestContext, r as resourceToTools, s as invokeController, t as filterResourcesForMcp, u as toCallToolError } from "../../mcpPlugin-
|
|
1
|
+
import { a as defaultCrudDescription, c as mcpHandlerAdapter, d as toCallToolResult, f as toCallToolSuccess, h as resolveToolCollisions, i as fieldRulesToZod, l as permissionDeniedResult, m as createMcpServer, n as mcpPlugin, o as resolveCrudDescription, p as buildRequestContext, r as resourceToTools, s as invokeController, t as filterResourcesForMcp, u as toCallToolError } from "../../mcpPlugin-BrepZ0cV.mjs";
|
|
2
2
|
//#region src/integrations/mcp/defineTool.ts
|
|
3
3
|
/**
|
|
4
4
|
* Define a type-safe MCP tool.
|
|
@@ -275,4 +275,104 @@ function definePrompt(name, config) {
|
|
|
275
275
|
};
|
|
276
276
|
}
|
|
277
277
|
//#endregion
|
|
278
|
-
|
|
278
|
+
//#region src/integrations/mcp/realtimeBridge.ts
|
|
279
|
+
/**
|
|
280
|
+
* Dispatch a batch of tool calls through the host's MCP tool registry.
|
|
281
|
+
*
|
|
282
|
+
* Returns the response array in the same order as the input. Each
|
|
283
|
+
* response carries either the tool's `content` (success path) or a
|
|
284
|
+
* `{isError:true, error:{message,...}}` shape (validation failure or
|
|
285
|
+
* handler throw). The realtime client uses the `id` to correlate
|
|
286
|
+
* responses to the original calls.
|
|
287
|
+
*/
|
|
288
|
+
async function dispatchRealtimeToolCall(tools, calls, opts = {}) {
|
|
289
|
+
const byName = new Map(tools.map((t) => [t.name, t]));
|
|
290
|
+
const noopLog = async () => {};
|
|
291
|
+
const baseCtx = {
|
|
292
|
+
session: opts.session ?? null,
|
|
293
|
+
log: opts.log ?? noopLog,
|
|
294
|
+
extra: opts.extra ?? {}
|
|
295
|
+
};
|
|
296
|
+
return (await Promise.allSettled(calls.map((call) => dispatchOne(byName.get(call.name), call, baseCtx)))).map((s, i) => {
|
|
297
|
+
const call = calls[i];
|
|
298
|
+
if (!call) return {
|
|
299
|
+
id: "unknown",
|
|
300
|
+
name: "unknown",
|
|
301
|
+
isError: true,
|
|
302
|
+
response: { error: "missing call slot" }
|
|
303
|
+
};
|
|
304
|
+
if (s.status === "fulfilled") return s.value;
|
|
305
|
+
return {
|
|
306
|
+
id: call.id,
|
|
307
|
+
name: call.name,
|
|
308
|
+
isError: true,
|
|
309
|
+
response: { error: s.reason instanceof Error ? s.reason.message : String(s.reason) }
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
async function dispatchOne(def, call, ctx) {
|
|
314
|
+
if (!def) return {
|
|
315
|
+
id: call.id,
|
|
316
|
+
name: call.name,
|
|
317
|
+
isError: true,
|
|
318
|
+
response: { error: `unknown tool: ${call.name}` }
|
|
319
|
+
};
|
|
320
|
+
let parsedArgs = call.arguments ?? {};
|
|
321
|
+
if (!call.arguments && typeof call.arguments_json === "string") try {
|
|
322
|
+
parsedArgs = JSON.parse(call.arguments_json);
|
|
323
|
+
} catch {
|
|
324
|
+
return {
|
|
325
|
+
id: call.id,
|
|
326
|
+
name: call.name,
|
|
327
|
+
isError: true,
|
|
328
|
+
response: { error: "arguments_json is not valid JSON" }
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (def.inputSchema) {
|
|
332
|
+
const fieldErrors = [];
|
|
333
|
+
const validated = {};
|
|
334
|
+
for (const [key, schema] of Object.entries(def.inputSchema)) {
|
|
335
|
+
const result = schema.safeParse(parsedArgs[key]);
|
|
336
|
+
if (!result.success) fieldErrors.push(`${key}: ${result.error.message}`);
|
|
337
|
+
else validated[key] = result.data;
|
|
338
|
+
}
|
|
339
|
+
if (fieldErrors.length > 0) return {
|
|
340
|
+
id: call.id,
|
|
341
|
+
name: call.name,
|
|
342
|
+
isError: true,
|
|
343
|
+
response: {
|
|
344
|
+
error: "argument validation failed",
|
|
345
|
+
fields: fieldErrors
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
parsedArgs = validated;
|
|
349
|
+
}
|
|
350
|
+
const result = await def.handler(parsedArgs, ctx);
|
|
351
|
+
return {
|
|
352
|
+
id: call.id,
|
|
353
|
+
name: call.name,
|
|
354
|
+
isError: result.isError,
|
|
355
|
+
response: normalizeResult(result)
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Normalize an MCP `CallToolResult` into a flat JSON object the realtime
|
|
360
|
+
* wire can carry. Prefers structuredContent; falls back to concatenated
|
|
361
|
+
* text content.
|
|
362
|
+
*/
|
|
363
|
+
function normalizeResult(result) {
|
|
364
|
+
if (result.structuredContent) return result.structuredContent;
|
|
365
|
+
const texts = [];
|
|
366
|
+
for (const part of result.content) if (part.type === "text" && typeof part.text === "string") texts.push(part.text);
|
|
367
|
+
if (texts.length > 0) {
|
|
368
|
+
const joined = texts.join("");
|
|
369
|
+
try {
|
|
370
|
+
const parsed = JSON.parse(joined);
|
|
371
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
372
|
+
} catch {}
|
|
373
|
+
return { text: joined };
|
|
374
|
+
}
|
|
375
|
+
return {};
|
|
376
|
+
}
|
|
377
|
+
//#endregion
|
|
378
|
+
export { bridgeToMcp, buildMcpToolsFromBridges, buildRequestContext, createMcpAuthFromBetterAuthApiKey, createMcpServer, customGuard, defaultCrudDescription, definePrompt, defineTool, denied, dispatchRealtimeToolCall, fieldRulesToZod, filterResourcesForMcp, getOrgId, getUserId, guard, hasOrg, invokeController, isAuthenticated, isOrg, mcpHandlerAdapter, mcpPlugin, permissionDeniedResult, requireAuth, requireOrg, requireOrgId, requireRole, resolveCrudDescription, resolveToolCollisions, resourceToTools, toCallToolError, toCallToolResult, toCallToolSuccess };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { m as createMcpServer, r as resourceToTools, t as filterResourcesForMcp } from "../../mcpPlugin-
|
|
1
|
+
import { m as createMcpServer, r as resourceToTools, t as filterResourcesForMcp } from "../../mcpPlugin-BrepZ0cV.mjs";
|
|
2
2
|
//#region src/integrations/mcp/testing.ts
|
|
3
3
|
/**
|
|
4
4
|
* @classytic/arc/mcp/testing — MCP Test Utilities
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { c as SerializedEntry, i as Principal, n as FencedWriteOutcome, s as PushRefStore, t as EnvelopeSnapshot } from "../pushref-registry-Bq_-EVsC.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/integrations/websocket-pushref-redis.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Minimal `ioredis` shape — typed structurally so this file doesn't
|
|
6
|
+
* take a typecheck-time dep on `ioredis`. The peer dep is declared
|
|
7
|
+
* `optional: true` in package.json; hosts who don't use this subpath
|
|
8
|
+
* never need to install ioredis.
|
|
9
|
+
*/
|
|
10
|
+
interface RedisLike {
|
|
11
|
+
get(key: string): Promise<string | null>;
|
|
12
|
+
set(key: string, value: string, mode?: string, duration?: number): Promise<unknown>;
|
|
13
|
+
del(key: string): Promise<number>;
|
|
14
|
+
pexpire(key: string, ttlMs: number): Promise<number>;
|
|
15
|
+
scan(cursor: string | number, matchKw: "MATCH", pattern: string, countKw: "COUNT", count: number): Promise<[string, string[]]>;
|
|
16
|
+
eval(script: string, numKeys: number, ...args: (string | number)[]): Promise<unknown>;
|
|
17
|
+
}
|
|
18
|
+
interface RedisPushRefStoreOptions {
|
|
19
|
+
/** Redis key namespace. Default `arc:pushref:`. */
|
|
20
|
+
keyPrefix?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Grace ms added to the Redis-side TTL beyond the registry's logical
|
|
23
|
+
* TTL — protects against clock skew between application and broker.
|
|
24
|
+
* Default 5_000.
|
|
25
|
+
*/
|
|
26
|
+
graceMs?: number;
|
|
27
|
+
}
|
|
28
|
+
declare class RedisPushRefStore implements PushRefStore {
|
|
29
|
+
private redis;
|
|
30
|
+
private prefix;
|
|
31
|
+
private graceMs;
|
|
32
|
+
constructor(redis: RedisLike, opts?: RedisPushRefStoreOptions);
|
|
33
|
+
get(pushRef: string): Promise<SerializedEntry | undefined>;
|
|
34
|
+
set(pushRef: string, entry: SerializedEntry): Promise<void>;
|
|
35
|
+
delete(pushRef: string): Promise<void>;
|
|
36
|
+
claim(args: {
|
|
37
|
+
pushRef: string;
|
|
38
|
+
principal: Principal;
|
|
39
|
+
now: number;
|
|
40
|
+
ttlMs: number;
|
|
41
|
+
mintFresh: () => SerializedEntry;
|
|
42
|
+
maxEntries: number;
|
|
43
|
+
countNonExpired: () => Promise<number>;
|
|
44
|
+
}): Promise<{
|
|
45
|
+
outcome: "new" | "resumed" | "rejected";
|
|
46
|
+
entry?: SerializedEntry;
|
|
47
|
+
supersededActive?: boolean;
|
|
48
|
+
}>;
|
|
49
|
+
releaseIfGen(args: {
|
|
50
|
+
pushRef: string;
|
|
51
|
+
expectedGeneration: number;
|
|
52
|
+
snapshot?: EnvelopeSnapshot;
|
|
53
|
+
expiresAt: number;
|
|
54
|
+
}): Promise<FencedWriteOutcome>;
|
|
55
|
+
persistIfGen(args: {
|
|
56
|
+
pushRef: string;
|
|
57
|
+
expectedGeneration: number;
|
|
58
|
+
snapshot: EnvelopeSnapshot;
|
|
59
|
+
expiresAt: number;
|
|
60
|
+
}): Promise<FencedWriteOutcome>;
|
|
61
|
+
stageIfInactive(args: {
|
|
62
|
+
pushRef: string;
|
|
63
|
+
payload: unknown;
|
|
64
|
+
timestamp: number;
|
|
65
|
+
expiresAt: number;
|
|
66
|
+
}): Promise<"ok" | "missing" | "active" | "raw">;
|
|
67
|
+
gc(_now: number): Promise<number>;
|
|
68
|
+
size(): Promise<number>;
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { RedisLike, RedisPushRefStore, RedisPushRefStoreOptions };
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
//#region src/integrations/websocket-pushref-redis.ts
|
|
2
|
+
/**
|
|
3
|
+
* Lua script for atomic claim. Runs server-side, single-roundtrip.
|
|
4
|
+
*
|
|
5
|
+
* KEYS[1] = key
|
|
6
|
+
* ARGV[1] = principal
|
|
7
|
+
* ARGV[2] = now (ms)
|
|
8
|
+
* ARGV[3] = ttlMs
|
|
9
|
+
* ARGV[4] = mintedJson (serialized SerializedEntry for new-claim path)
|
|
10
|
+
* ARGV[5] = graceMs (added to PEXPIRE so the broker GC's stale keys)
|
|
11
|
+
*
|
|
12
|
+
* Returns: { outcome, json, supersededActive } where
|
|
13
|
+
* outcome ∈ {"new","resumed","rejected"}
|
|
14
|
+
* json is the SerializedEntry (or empty string for reject)
|
|
15
|
+
* supersededActive is "1" when the previous entry was still active
|
|
16
|
+
*
|
|
17
|
+
* Generation handling: every resumed claim bumps `entry.generation` so
|
|
18
|
+
* the connection layer can fence the previous owner. The bump happens
|
|
19
|
+
* atomically inside the script — no read-modify-write race across nodes.
|
|
20
|
+
*/
|
|
21
|
+
const CLAIM_SCRIPT = `
|
|
22
|
+
local existing = redis.call('GET', KEYS[1])
|
|
23
|
+
local now = tonumber(ARGV[2])
|
|
24
|
+
local ttl = tonumber(ARGV[3])
|
|
25
|
+
local pexpire = ttl + tonumber(ARGV[5])
|
|
26
|
+
if existing then
|
|
27
|
+
local entry = cjson.decode(existing)
|
|
28
|
+
if entry.active == false and entry.expiresAt <= now then
|
|
29
|
+
-- expired — fall through to new-claim path
|
|
30
|
+
else
|
|
31
|
+
if entry.principal ~= ARGV[1] then
|
|
32
|
+
return {'rejected', '', '0'}
|
|
33
|
+
end
|
|
34
|
+
local superseded = '0'
|
|
35
|
+
if entry.active == true then
|
|
36
|
+
superseded = '1'
|
|
37
|
+
end
|
|
38
|
+
entry.active = true
|
|
39
|
+
entry.expiresAt = now + ttl
|
|
40
|
+
entry.generation = (entry.generation or 0) + 1
|
|
41
|
+
local payload = cjson.encode(entry)
|
|
42
|
+
redis.call('SET', KEYS[1], payload, 'PX', pexpire)
|
|
43
|
+
return {'resumed', payload, superseded}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
redis.call('SET', KEYS[1], ARGV[4], 'PX', pexpire)
|
|
47
|
+
return {'new', ARGV[4], '0'}
|
|
48
|
+
`;
|
|
49
|
+
/**
|
|
50
|
+
* Generation-fenced release. Single Lua roundtrip:
|
|
51
|
+
*
|
|
52
|
+
* KEYS[1] = key
|
|
53
|
+
* ARGV[1] = expectedGeneration
|
|
54
|
+
* ARGV[2] = expiresAt (ms epoch)
|
|
55
|
+
* ARGV[3] = pexpire ms (relative)
|
|
56
|
+
* ARGV[4] = snapshotJson or empty string (skip envelope flush when empty)
|
|
57
|
+
*
|
|
58
|
+
* Returns "ok" | "stale" | "missing".
|
|
59
|
+
*
|
|
60
|
+
* Stale outcome means another node has claimed a higher generation
|
|
61
|
+
* since the caller captured theirs — caller's release is dropped at
|
|
62
|
+
* the store level. This closes the 2.17.2 race where a fenced Node A's
|
|
63
|
+
* close handler could mutate Node B's freshly-claimed entry.
|
|
64
|
+
*/
|
|
65
|
+
const RELEASE_IF_GEN_SCRIPT = `
|
|
66
|
+
local existing = redis.call('GET', KEYS[1])
|
|
67
|
+
if not existing then return 'missing' end
|
|
68
|
+
local entry = cjson.decode(existing)
|
|
69
|
+
local expected = tonumber(ARGV[1])
|
|
70
|
+
if entry.generation ~= expected then return 'stale' end
|
|
71
|
+
entry.active = false
|
|
72
|
+
entry.expiresAt = tonumber(ARGV[2])
|
|
73
|
+
if ARGV[4] ~= '' then
|
|
74
|
+
local snap = cjson.decode(ARGV[4])
|
|
75
|
+
entry.nextSeq = snap.nextSeq
|
|
76
|
+
entry.deadQueue = snap.deadQueue
|
|
77
|
+
end
|
|
78
|
+
redis.call('SET', KEYS[1], cjson.encode(entry), 'PX', tonumber(ARGV[3]))
|
|
79
|
+
return 'ok'
|
|
80
|
+
`;
|
|
81
|
+
/**
|
|
82
|
+
* Atomic offline-stage. Closes the read-modify-write race the 2.17.2
|
|
83
|
+
* review surfaced for `stageForResume` — a get-then-set could clobber
|
|
84
|
+
* a freshly-claimed higher-generation entry with the stale inactive
|
|
85
|
+
* snapshot. The whole sequence (test active + seq mode, assign seq,
|
|
86
|
+
* wrap payload, append, trim, extend TTL) runs in one EVAL.
|
|
87
|
+
*
|
|
88
|
+
* KEYS[1] = key
|
|
89
|
+
* ARGV[1] = payloadJson (raw payload, NOT pre-wrapped)
|
|
90
|
+
* ARGV[2] = timestamp (ms, server clock)
|
|
91
|
+
* ARGV[3] = expiresAt (ms epoch)
|
|
92
|
+
* ARGV[4] = pexpire ms
|
|
93
|
+
*
|
|
94
|
+
* Returns "ok" | "missing" | "active" | "raw".
|
|
95
|
+
*
|
|
96
|
+
* Note on JSON key order: cjson.encode and JSON.stringify may emit
|
|
97
|
+
* different orderings; consumers parse the envelope and read by key,
|
|
98
|
+
* so order is not part of the wire contract.
|
|
99
|
+
*/
|
|
100
|
+
const STAGE_IF_INACTIVE_SCRIPT = `
|
|
101
|
+
local existing = redis.call('GET', KEYS[1])
|
|
102
|
+
if not existing then return 'missing' end
|
|
103
|
+
local entry = cjson.decode(existing)
|
|
104
|
+
if entry.active then return 'active' end
|
|
105
|
+
if entry.envelopeMode ~= 'seq' then return 'raw' end
|
|
106
|
+
local seq = entry.nextSeq
|
|
107
|
+
entry.nextSeq = seq + 1
|
|
108
|
+
local payload = cjson.decode(ARGV[1])
|
|
109
|
+
local envelope = { seq = seq, t = tonumber(ARGV[2]), msg = payload }
|
|
110
|
+
local serialized = cjson.encode(envelope)
|
|
111
|
+
if not entry.deadQueue then entry.deadQueue = {} end
|
|
112
|
+
table.insert(entry.deadQueue, { seq = seq, payload = serialized })
|
|
113
|
+
while #entry.deadQueue > entry.deadQueueSize do
|
|
114
|
+
table.remove(entry.deadQueue, 1)
|
|
115
|
+
end
|
|
116
|
+
entry.expiresAt = tonumber(ARGV[3])
|
|
117
|
+
redis.call('SET', KEYS[1], cjson.encode(entry), 'PX', tonumber(ARGV[4]))
|
|
118
|
+
return 'ok'
|
|
119
|
+
`;
|
|
120
|
+
/**
|
|
121
|
+
* Generation-fenced persist. Same atomicity contract as release.
|
|
122
|
+
*
|
|
123
|
+
* KEYS[1] = key
|
|
124
|
+
* ARGV[1] = expectedGeneration
|
|
125
|
+
* ARGV[2] = expiresAt
|
|
126
|
+
* ARGV[3] = pexpire ms
|
|
127
|
+
* ARGV[4] = snapshotJson (required for persist — non-empty)
|
|
128
|
+
*/
|
|
129
|
+
const PERSIST_IF_GEN_SCRIPT = `
|
|
130
|
+
local existing = redis.call('GET', KEYS[1])
|
|
131
|
+
if not existing then return 'missing' end
|
|
132
|
+
local entry = cjson.decode(existing)
|
|
133
|
+
local expected = tonumber(ARGV[1])
|
|
134
|
+
if entry.generation ~= expected then return 'stale' end
|
|
135
|
+
local snap = cjson.decode(ARGV[4])
|
|
136
|
+
entry.nextSeq = snap.nextSeq
|
|
137
|
+
entry.deadQueue = snap.deadQueue
|
|
138
|
+
entry.expiresAt = tonumber(ARGV[2])
|
|
139
|
+
redis.call('SET', KEYS[1], cjson.encode(entry), 'PX', tonumber(ARGV[3]))
|
|
140
|
+
return 'ok'
|
|
141
|
+
`;
|
|
142
|
+
var RedisPushRefStore = class {
|
|
143
|
+
redis;
|
|
144
|
+
prefix;
|
|
145
|
+
graceMs;
|
|
146
|
+
constructor(redis, opts = {}) {
|
|
147
|
+
this.redis = redis;
|
|
148
|
+
this.prefix = opts.keyPrefix ?? "arc:pushref:";
|
|
149
|
+
this.graceMs = opts.graceMs ?? 5e3;
|
|
150
|
+
}
|
|
151
|
+
async get(pushRef) {
|
|
152
|
+
const raw = await this.redis.get(this.prefix + pushRef);
|
|
153
|
+
if (!raw) return void 0;
|
|
154
|
+
return JSON.parse(raw);
|
|
155
|
+
}
|
|
156
|
+
async set(pushRef, entry) {
|
|
157
|
+
const json = JSON.stringify(entry);
|
|
158
|
+
const pexpire = Math.max(1e3, entry.expiresAt - Date.now() + this.graceMs);
|
|
159
|
+
await this.redis.set(this.prefix + pushRef, json, "PX", pexpire);
|
|
160
|
+
}
|
|
161
|
+
async delete(pushRef) {
|
|
162
|
+
await this.redis.del(this.prefix + pushRef);
|
|
163
|
+
}
|
|
164
|
+
async claim(args) {
|
|
165
|
+
if (args.maxEntries > 0) {
|
|
166
|
+
if (await args.countNonExpired() >= args.maxEntries) {}
|
|
167
|
+
}
|
|
168
|
+
const minted = JSON.stringify(args.mintFresh());
|
|
169
|
+
const result = await this.redis.eval(CLAIM_SCRIPT, 1, this.prefix + args.pushRef, args.principal, args.now, args.ttlMs, minted, this.graceMs);
|
|
170
|
+
const outcome = result[0];
|
|
171
|
+
if (outcome === "rejected") return { outcome };
|
|
172
|
+
return {
|
|
173
|
+
outcome,
|
|
174
|
+
entry: JSON.parse(result[1]),
|
|
175
|
+
supersededActive: result[2] === "1"
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
async releaseIfGen(args) {
|
|
179
|
+
const pexpire = Math.max(1e3, args.expiresAt - Date.now() + this.graceMs);
|
|
180
|
+
return await this.redis.eval(RELEASE_IF_GEN_SCRIPT, 1, this.prefix + args.pushRef, args.expectedGeneration, args.expiresAt, pexpire, args.snapshot ? JSON.stringify(args.snapshot) : "");
|
|
181
|
+
}
|
|
182
|
+
async persistIfGen(args) {
|
|
183
|
+
const pexpire = Math.max(1e3, args.expiresAt - Date.now() + this.graceMs);
|
|
184
|
+
return await this.redis.eval(PERSIST_IF_GEN_SCRIPT, 1, this.prefix + args.pushRef, args.expectedGeneration, args.expiresAt, pexpire, JSON.stringify(args.snapshot));
|
|
185
|
+
}
|
|
186
|
+
async stageIfInactive(args) {
|
|
187
|
+
const pexpire = Math.max(1e3, args.expiresAt - Date.now() + this.graceMs);
|
|
188
|
+
return await this.redis.eval(STAGE_IF_INACTIVE_SCRIPT, 1, this.prefix + args.pushRef, JSON.stringify(args.payload), args.timestamp, args.expiresAt, pexpire);
|
|
189
|
+
}
|
|
190
|
+
async gc(_now) {
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
async size() {
|
|
194
|
+
let cursor = "0";
|
|
195
|
+
let count = 0;
|
|
196
|
+
do {
|
|
197
|
+
const [next, keys] = await this.redis.scan(cursor, "MATCH", `${this.prefix}*`, "COUNT", 500);
|
|
198
|
+
count += keys.length;
|
|
199
|
+
cursor = next;
|
|
200
|
+
} while (cursor !== "0");
|
|
201
|
+
return count;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
//#endregion
|
|
205
|
+
export { RedisPushRefStore };
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
|
|
1
|
+
import { a as GeoRoomOptions, c as WebSocketClient, d as LocalWebSocketAdapter, f as WebSocketAdapter, i as GeoRoom, l as WebSocketMessage, n as GeoPoint, o as RoomManager, r as GeoPublishOpts, s as AuthResult, t as websocketPlugin, u as WebSocketPluginOptions } from "../websocket-BZMUP7Gr.mjs";
|
|
2
|
+
import { a as PushRefRegistry, c as SerializedEntry, i as Principal, o as PushRefRegistryOptions, r as MemoryPushRefStore, s as PushRefStore } from "../pushref-registry-Bq_-EVsC.mjs";
|
|
3
|
+
export { AuthResult, GeoPoint, GeoPublishOpts, GeoRoom, GeoRoomOptions, LocalWebSocketAdapter, MemoryPushRefStore, Principal, PushRefRegistry, PushRefRegistryOptions, PushRefStore, RoomManager, SerializedEntry, WebSocketAdapter, WebSocketClient, WebSocketMessage, WebSocketPluginOptions, websocketPlugin };
|