@browserlessapi/inferlo 0.6.0 → 0.7.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 +7 -4
- package/dist/ai/ai-sdk.d.ts +48 -0
- package/dist/ai/ai-sdk.js +235 -0
- package/dist/ai/ai-sdk.js.map +1 -0
- package/dist/ai/index.d.ts +37 -3
- package/dist/ai/index.js +96 -7
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/public.d.ts +5 -1
- package/dist/ai/public.js +2 -0
- package/dist/ai/public.js.map +1 -1
- package/dist/ai/ui.d.ts +47 -0
- package/dist/ai/ui.js +33 -0
- package/dist/ai/ui.js.map +1 -0
- package/dist/app.d.ts +31 -4
- package/dist/app.js +106 -29
- package/dist/app.js.map +1 -1
- package/dist/astro.d.ts +60 -0
- package/dist/astro.js +68 -0
- package/dist/astro.js.map +1 -0
- package/dist/audit.d.ts +8 -1
- package/dist/audit.js +9 -2
- package/dist/audit.js.map +1 -1
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +1 -0
- package/dist/auth.js.map +1 -1
- package/dist/azure.d.ts +66 -0
- package/dist/azure.js +119 -0
- package/dist/azure.js.map +1 -0
- package/dist/cache.d.ts +13 -9
- package/dist/cache.js +11 -9
- package/dist/cache.js.map +1 -1
- package/dist/cli/main.js +4 -4
- package/dist/cli/main.js.map +1 -1
- package/dist/client-ip.d.ts +36 -1
- package/dist/client-ip.js +80 -1
- package/dist/client-ip.js.map +1 -1
- package/dist/db/memory.js +2 -1
- package/dist/db/memory.js.map +1 -1
- package/dist/db/postgres.js +4 -1
- package/dist/db/postgres.js.map +1 -1
- package/dist/db/table.d.ts +37 -5
- package/dist/db/table.js +96 -15
- package/dist/db/table.js.map +1 -1
- package/dist/db/types.d.ts +2 -0
- package/dist/describe.d.ts +15 -4
- package/dist/describe.js +10 -2
- package/dist/describe.js.map +1 -1
- package/dist/events.d.ts +134 -0
- package/dist/events.js +196 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/jobs.d.ts +50 -10
- package/dist/jobs.js +66 -13
- package/dist/jobs.js.map +1 -1
- package/dist/langchain.d.ts +58 -0
- package/dist/langchain.js +220 -0
- package/dist/langchain.js.map +1 -0
- package/dist/model.d.ts +23 -6
- package/dist/model.js +35 -6
- package/dist/model.js.map +1 -1
- package/dist/rate-limit.d.ts +5 -3
- package/dist/rate-limit.js +3 -3
- package/dist/rate-limit.js.map +1 -1
- package/dist/redis-events.d.ts +27 -0
- package/dist/redis-events.js +163 -0
- package/dist/redis-events.js.map +1 -0
- package/dist/redis-jobs.d.ts +24 -0
- package/dist/redis-jobs.js +203 -0
- package/dist/redis-jobs.js.map +1 -0
- package/dist/redis.d.ts +30 -0
- package/dist/redis.js +106 -0
- package/dist/redis.js.map +1 -0
- package/dist/response-body.d.ts +6 -0
- package/dist/response-body.js +7 -0
- package/dist/response-body.js.map +1 -0
- package/dist/route.d.ts +14 -1
- package/dist/route.js.map +1 -1
- package/dist/schema.d.ts +2 -2
- package/dist/serve.d.ts +6 -1
- package/dist/serve.js +64 -7
- package/dist/serve.js.map +1 -1
- package/dist/service-client.d.ts +91 -0
- package/dist/service-client.js +177 -0
- package/dist/service-client.js.map +1 -0
- package/dist/sessions.d.ts +2 -0
- package/dist/sessions.js +1 -0
- package/dist/sessions.js.map +1 -1
- package/dist/tracing.d.ts +7 -3
- package/dist/tracing.js +8 -2
- package/dist/tracing.js.map +1 -1
- package/llms.txt +1 -0
- package/package.json +36 -4
- package/templates/app/AGENTS.md +5 -3
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
export const EVENT_STREAM_PREFIX = "inferlo:events:";
|
|
2
|
+
/** Consumers with nothing pending that have not read for this long (stopped instances) are removed from the group. */
|
|
3
|
+
const IDLE_CONSUMER_MS = 60 * 60_000;
|
|
4
|
+
/** Field-value pairs as Redis sends them: a flat array (RESP2), or an object or Map (RESP3 clients). */
|
|
5
|
+
function fieldMap(fields) {
|
|
6
|
+
const map = new Map();
|
|
7
|
+
if (Array.isArray(fields)) {
|
|
8
|
+
for (let i = 0; i + 1 < fields.length; i += 2)
|
|
9
|
+
map.set(String(fields[i]), String(fields[i + 1]));
|
|
10
|
+
}
|
|
11
|
+
else if (fields instanceof Map) {
|
|
12
|
+
for (const [k, v] of fields)
|
|
13
|
+
map.set(String(k), String(v));
|
|
14
|
+
}
|
|
15
|
+
else if (typeof fields === "object" && fields !== null) {
|
|
16
|
+
for (const [k, v] of Object.entries(fields))
|
|
17
|
+
map.set(k, String(v));
|
|
18
|
+
}
|
|
19
|
+
else
|
|
20
|
+
return undefined;
|
|
21
|
+
return map;
|
|
22
|
+
}
|
|
23
|
+
/** The entries of an XREADGROUP reply: [[key, entries], ...] (RESP2), or { key: entries } / Map (RESP3). */
|
|
24
|
+
function streamEntries(reply) {
|
|
25
|
+
if (reply === null || reply === undefined)
|
|
26
|
+
return [];
|
|
27
|
+
const lists = Array.isArray(reply)
|
|
28
|
+
? reply.map((r) => (Array.isArray(r) ? r[1] : undefined))
|
|
29
|
+
: reply instanceof Map
|
|
30
|
+
? [...reply.values()]
|
|
31
|
+
: typeof reply === "object"
|
|
32
|
+
? Object.values(reply)
|
|
33
|
+
: [];
|
|
34
|
+
return lists.flatMap((list) => (Array.isArray(list) ? list : []));
|
|
35
|
+
}
|
|
36
|
+
function errorText(error) {
|
|
37
|
+
return error instanceof Error ? error.message : String(error);
|
|
38
|
+
}
|
|
39
|
+
export function redisEventTransport(client, options) {
|
|
40
|
+
const group = options.group;
|
|
41
|
+
const maxLength = options.maxLength ?? 100_000;
|
|
42
|
+
const staleAfterMs = options.staleAfterMs ?? 30_000;
|
|
43
|
+
const batch = options.batch ?? 100;
|
|
44
|
+
// One consumer per transport (per app instance). Stopped instances' consumers are removed after IDLE_CONSUMER_MS.
|
|
45
|
+
const consumer = `${group}:${crypto.randomUUID()}`;
|
|
46
|
+
const ready = new Set();
|
|
47
|
+
let lastCleanup = 0;
|
|
48
|
+
const stream = (name) => `${EVENT_STREAM_PREFIX}${name}`;
|
|
49
|
+
/** Creates the group at the end of the stream (and the stream) the first time this instance reads it. */
|
|
50
|
+
const ensureGroup = async (key) => {
|
|
51
|
+
if (ready.has(key))
|
|
52
|
+
return;
|
|
53
|
+
try {
|
|
54
|
+
await client.sendCommand(["XGROUP", "CREATE", key, group, "$", "MKSTREAM"]);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (!/BUSYGROUP/.test(errorText(error)))
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
ready.add(key);
|
|
61
|
+
};
|
|
62
|
+
const parse = (name, id, fields) => {
|
|
63
|
+
const eventId = fields.get("id");
|
|
64
|
+
const payload = fields.get("payload");
|
|
65
|
+
if (!eventId || payload === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
let value;
|
|
68
|
+
try {
|
|
69
|
+
value = JSON.parse(payload);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const at = new Date(fields.get("publishedAt") ?? Number(id.split("-")[0]));
|
|
75
|
+
const traceparent = fields.get("traceparent");
|
|
76
|
+
const tracestate = fields.get("tracestate");
|
|
77
|
+
return {
|
|
78
|
+
id: eventId,
|
|
79
|
+
name,
|
|
80
|
+
payload: value,
|
|
81
|
+
publishedAt: Number.isNaN(at.getTime()) ? new Date() : at,
|
|
82
|
+
...(traceparent ? { trace: { traceparent, ...(tracestate ? { tracestate } : {}) } } : {}),
|
|
83
|
+
...(fields.get("tenantId") ? { tenantId: fields.get("tenantId") } : {}),
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
/** Removes consumers of stopped instances: nothing pending and idle for an hour. */
|
|
87
|
+
const cleanup = async (key) => {
|
|
88
|
+
const reply = await client.sendCommand(["XINFO", "CONSUMERS", key, group]);
|
|
89
|
+
for (const row of Array.isArray(reply) ? reply : []) {
|
|
90
|
+
const info = fieldMap(row);
|
|
91
|
+
if (!info)
|
|
92
|
+
continue;
|
|
93
|
+
const name = info.get("name");
|
|
94
|
+
if (name && name !== consumer && Number(info.get("pending")) === 0 && Number(info.get("idle")) > IDLE_CONSUMER_MS) {
|
|
95
|
+
await client.sendCommand(["XGROUP", "DELCONSUMER", key, group, name]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
name: "redis",
|
|
101
|
+
async send(message) {
|
|
102
|
+
const fields = ["id", message.id, "payload", JSON.stringify(message.payload ?? null), "publishedAt", message.publishedAt.toISOString()];
|
|
103
|
+
if (message.trace?.traceparent)
|
|
104
|
+
fields.push("traceparent", message.trace.traceparent);
|
|
105
|
+
if (message.trace?.tracestate)
|
|
106
|
+
fields.push("tracestate", message.trace.tracestate);
|
|
107
|
+
if (message.tenantId)
|
|
108
|
+
fields.push("tenantId", message.tenantId);
|
|
109
|
+
await client.sendCommand(["XADD", stream(message.name), "MAXLEN", "~", String(maxLength), "*", ...fields]);
|
|
110
|
+
},
|
|
111
|
+
async receive(names, deliver) {
|
|
112
|
+
let delivered = 0;
|
|
113
|
+
let failure;
|
|
114
|
+
const handle = async (key, name, entry) => {
|
|
115
|
+
if (!Array.isArray(entry))
|
|
116
|
+
return;
|
|
117
|
+
const id = String(entry[0]);
|
|
118
|
+
const fields = Array.isArray(entry[1]) ? fieldMap(entry[1]) : undefined;
|
|
119
|
+
const message = fields ? parse(name, id, fields) : undefined;
|
|
120
|
+
if (message) {
|
|
121
|
+
try {
|
|
122
|
+
await deliver(message);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
failure ??= error; // left pending: delivered again after staleAfterMs
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
delivered++;
|
|
129
|
+
}
|
|
130
|
+
// Delivered, trimmed away before it was read again, or not an Inferlo event: acknowledge it.
|
|
131
|
+
await client.sendCommand(["XACK", key, group, id]);
|
|
132
|
+
};
|
|
133
|
+
const cleanupDue = Date.now() - lastCleanup > IDLE_CONSUMER_MS / 4;
|
|
134
|
+
if (cleanupDue)
|
|
135
|
+
lastCleanup = Date.now();
|
|
136
|
+
for (const name of names) {
|
|
137
|
+
const key = stream(name);
|
|
138
|
+
try {
|
|
139
|
+
await ensureGroup(key);
|
|
140
|
+
// Entries another instance read but never acknowledged (it stopped): claim them for this one.
|
|
141
|
+
const claimed = (await client.sendCommand(["XAUTOCLAIM", key, group, consumer, String(staleAfterMs), "0-0", "COUNT", String(batch)]));
|
|
142
|
+
for (const entry of Array.isArray(claimed?.[1]) ? claimed[1] : [])
|
|
143
|
+
await handle(key, name, entry);
|
|
144
|
+
const read = await client.sendCommand(["XREADGROUP", "GROUP", group, consumer, "COUNT", String(batch), "STREAMS", key, ">"]);
|
|
145
|
+
for (const entry of streamEntries(read))
|
|
146
|
+
await handle(key, name, entry);
|
|
147
|
+
if (cleanupDue)
|
|
148
|
+
await cleanup(key);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
// The stream or the group was deleted (for example by FLUSHDB): create them again on the next call.
|
|
152
|
+
if (/NOGROUP/.test(errorText(error)))
|
|
153
|
+
ready.delete(key);
|
|
154
|
+
failure ??= error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (failure !== undefined)
|
|
158
|
+
throw failure;
|
|
159
|
+
return delivered;
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=redis-events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-events.js","sourceRoot":"","sources":["../src/redis-events.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Redis job queue: createApp({ redis, jobQueue: { store: "redis" } }).
|
|
3
|
+
*
|
|
4
|
+
* Keys (prefix "inferlo:<app name>:"):
|
|
5
|
+
* jobs:job:<id> a hash with the job record
|
|
6
|
+
* jobs:due:<name> a sorted set of the pending jobs of one job name, scored by runAt (ms)
|
|
7
|
+
* jobs:running a sorted set of running jobs, scored by the last report from their worker (Redis clock, ms)
|
|
8
|
+
*
|
|
9
|
+
* Every change is one Lua script, so a job is claimed by one worker only. Times that decide whether a running
|
|
10
|
+
* job is stuck come from the Redis server clock (TIME), so instances with different clocks agree.
|
|
11
|
+
*/
|
|
12
|
+
import { type JobStore } from "./jobs.ts";
|
|
13
|
+
import type { RedisClient } from "./redis.ts";
|
|
14
|
+
/** Finished (done or failed) jobs are kept this long, then Redis removes them. */
|
|
15
|
+
export declare const REDIS_FINISHED_TTL_MS: number;
|
|
16
|
+
/**
|
|
17
|
+
* Jobs in Redis, shared by every instance, with the guarantees of the database queue: each due job is claimed by
|
|
18
|
+
* one worker (one atomic script), failed attempts are retried with backoff, a running job whose worker stopped
|
|
19
|
+
* reporting for staleAfterMs is claimed again (or failed without attempts left), and cron ticks are queued once.
|
|
20
|
+
*/
|
|
21
|
+
export declare function redisJobStore(client: RedisClient, prefix: string, options?: {
|
|
22
|
+
staleAfterMs?: number;
|
|
23
|
+
finishedTtlMs?: number;
|
|
24
|
+
}): JobStore;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Redis job queue: createApp({ redis, jobQueue: { store: "redis" } }).
|
|
3
|
+
*
|
|
4
|
+
* Keys (prefix "inferlo:<app name>:"):
|
|
5
|
+
* jobs:job:<id> a hash with the job record
|
|
6
|
+
* jobs:due:<name> a sorted set of the pending jobs of one job name, scored by runAt (ms)
|
|
7
|
+
* jobs:running a sorted set of running jobs, scored by the last report from their worker (Redis clock, ms)
|
|
8
|
+
*
|
|
9
|
+
* Every change is one Lua script, so a job is claimed by one worker only. Times that decide whether a running
|
|
10
|
+
* job is stuck come from the Redis server clock (TIME), so instances with different clocks agree.
|
|
11
|
+
*/
|
|
12
|
+
import { STALE_AFTER_MS, STOPPED_MESSAGE } from "./jobs.js";
|
|
13
|
+
/** Finished (done or failed) jobs are kept this long, then Redis removes them. */
|
|
14
|
+
export const REDIS_FINISHED_TTL_MS = 7 * 24 * 60 * 60_000;
|
|
15
|
+
const CLOCK = `local t = redis.call("TIME")
|
|
16
|
+
local clock = tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)`;
|
|
17
|
+
// KEYS[1] job hash, KEYS[2] due set of its name. ARGV[1] id, ARGV[2] runAt ms, ARGV[3] status, ARGV[4..] field pairs.
|
|
18
|
+
// A record whose id is already stored is ignored (cron ticks use fixed ids).
|
|
19
|
+
const ADD = `if redis.call("EXISTS", KEYS[1]) == 1 then return 0 end
|
|
20
|
+
redis.call("HSET", KEYS[1], unpack(ARGV, 4))
|
|
21
|
+
if ARGV[3] == "pending" then redis.call("ZADD", KEYS[2], ARGV[2], ARGV[1]) end
|
|
22
|
+
return 1`;
|
|
23
|
+
// KEYS[1] running set, KEYS[2..] due sets (one per name in ARGV[7..], same order).
|
|
24
|
+
// ARGV[1] now ms (runAt cutoff), ARGV[2] limit, ARGV[3] staleAfterMs, ARGV[4] stopped message,
|
|
25
|
+
// ARGV[5] job hash key prefix, ARGV[6] finished ttl ms.
|
|
26
|
+
const CLAIM = `${CLOCK}
|
|
27
|
+
local now = tonumber(ARGV[1])
|
|
28
|
+
local limit = tonumber(ARGV[2])
|
|
29
|
+
local dueKey = {}
|
|
30
|
+
for i = 7, #ARGV do dueKey[ARGV[i]] = KEYS[i - 5] end
|
|
31
|
+
local candidates = {}
|
|
32
|
+
-- Jobs whose worker stopped reporting: run again, or fail them when no attempts are left.
|
|
33
|
+
local stale = redis.call("ZRANGEBYSCORE", KEYS[1], "-inf", clock - tonumber(ARGV[3]), "LIMIT", 0, 1000)
|
|
34
|
+
for _, id in ipairs(stale) do
|
|
35
|
+
local key = ARGV[5] .. id
|
|
36
|
+
local h = redis.call("HMGET", key, "name", "attempts", "max_attempts", "run_at", "created_at", "status")
|
|
37
|
+
if not h[1] or h[6] ~= "running" then
|
|
38
|
+
redis.call("ZREM", KEYS[1], id)
|
|
39
|
+
elseif dueKey[h[1]] then
|
|
40
|
+
if tonumber(h[2]) >= tonumber(h[3]) then
|
|
41
|
+
redis.call("HSET", key, "status", "failed", "last_error", ARGV[4], "updated_at", clock)
|
|
42
|
+
redis.call("ZREM", KEYS[1], id)
|
|
43
|
+
redis.call("PEXPIRE", key, ARGV[6])
|
|
44
|
+
else
|
|
45
|
+
table.insert(candidates, { id, tonumber(h[4]), tonumber(h[5]), true, h[1] })
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
for i = 7, #ARGV do
|
|
50
|
+
local ids = redis.call("ZRANGEBYSCORE", KEYS[i - 5], "-inf", now, "WITHSCORES", "LIMIT", 0, limit)
|
|
51
|
+
for j = 1, #ids, 2 do
|
|
52
|
+
local created = redis.call("HGET", ARGV[5] .. ids[j], "created_at")
|
|
53
|
+
if created then
|
|
54
|
+
table.insert(candidates, { ids[j], tonumber(ids[j + 1]), tonumber(created), false, ARGV[i] })
|
|
55
|
+
else
|
|
56
|
+
redis.call("ZREM", KEYS[i - 5], ids[j])
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
table.sort(candidates, function(a, b)
|
|
61
|
+
if a[2] ~= b[2] then return a[2] < b[2] end
|
|
62
|
+
if a[3] ~= b[3] then return a[3] < b[3] end
|
|
63
|
+
return a[1] < b[1]
|
|
64
|
+
end)
|
|
65
|
+
local out = {}
|
|
66
|
+
for i = 1, math.min(limit, #candidates) do
|
|
67
|
+
local c = candidates[i]
|
|
68
|
+
local key = ARGV[5] .. c[1]
|
|
69
|
+
if c[4] then redis.call("HSET", key, "last_error", ARGV[4]) end
|
|
70
|
+
redis.call("ZREM", dueKey[c[5]], c[1])
|
|
71
|
+
redis.call("HSET", key, "status", "running", "updated_at", clock)
|
|
72
|
+
redis.call("HINCRBY", key, "attempts", 1)
|
|
73
|
+
redis.call("PERSIST", key)
|
|
74
|
+
redis.call("ZADD", KEYS[1], clock, c[1])
|
|
75
|
+
table.insert(out, redis.call("HGETALL", key))
|
|
76
|
+
end
|
|
77
|
+
return out`;
|
|
78
|
+
// KEYS[1] job hash, KEYS[2] running set. ARGV[1] id, ARGV[2] due key prefix, ARGV[3] finished ttl ms, ARGV[4..] field pairs.
|
|
79
|
+
const UPDATE = `${CLOCK}
|
|
80
|
+
if redis.call("EXISTS", KEYS[1]) == 0 then return 0 end
|
|
81
|
+
if #ARGV >= 4 then redis.call("HSET", KEYS[1], unpack(ARGV, 4)) end
|
|
82
|
+
redis.call("HSET", KEYS[1], "updated_at", clock)
|
|
83
|
+
local h = redis.call("HMGET", KEYS[1], "status", "run_at", "name")
|
|
84
|
+
local due = ARGV[2] .. h[3]
|
|
85
|
+
if h[1] == "pending" then
|
|
86
|
+
redis.call("ZREM", KEYS[2], ARGV[1])
|
|
87
|
+
redis.call("ZADD", due, h[2], ARGV[1])
|
|
88
|
+
redis.call("PERSIST", KEYS[1])
|
|
89
|
+
elseif h[1] == "running" then
|
|
90
|
+
redis.call("ZREM", due, ARGV[1])
|
|
91
|
+
redis.call("ZADD", KEYS[2], clock, ARGV[1])
|
|
92
|
+
else
|
|
93
|
+
redis.call("ZREM", KEYS[2], ARGV[1])
|
|
94
|
+
redis.call("ZREM", due, ARGV[1])
|
|
95
|
+
redis.call("PEXPIRE", KEYS[1], ARGV[3])
|
|
96
|
+
end
|
|
97
|
+
return 1`;
|
|
98
|
+
// KEYS[1] job hash, KEYS[2] running set. ARGV[1] id.
|
|
99
|
+
const TOUCH = `${CLOCK}
|
|
100
|
+
if redis.call("HGET", KEYS[1], "status") ~= "running" then return 0 end
|
|
101
|
+
redis.call("HSET", KEYS[1], "updated_at", clock)
|
|
102
|
+
redis.call("ZADD", KEYS[2], "XX", clock, ARGV[1])
|
|
103
|
+
return 1`;
|
|
104
|
+
/** HGETALL answers: a flat [field, value, ...] list (RESP2), or a Map or object (RESP3). */
|
|
105
|
+
function fields(reply) {
|
|
106
|
+
const out = {};
|
|
107
|
+
if (Array.isArray(reply)) {
|
|
108
|
+
for (let i = 0; i + 1 < reply.length; i += 2)
|
|
109
|
+
out[String(reply[i])] = String(reply[i + 1]);
|
|
110
|
+
}
|
|
111
|
+
else if (reply instanceof Map) {
|
|
112
|
+
for (const [k, v] of reply)
|
|
113
|
+
out[String(k)] = String(v);
|
|
114
|
+
}
|
|
115
|
+
else if (reply && typeof reply === "object") {
|
|
116
|
+
for (const [k, v] of Object.entries(reply))
|
|
117
|
+
out[k] = String(v);
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
function fromHash(id, h) {
|
|
122
|
+
const record = {
|
|
123
|
+
id,
|
|
124
|
+
name: h.name,
|
|
125
|
+
input: JSON.parse(h.input ?? "null"),
|
|
126
|
+
status: h.status,
|
|
127
|
+
attempts: Number(h.attempts),
|
|
128
|
+
maxAttempts: Number(h.max_attempts),
|
|
129
|
+
runAt: new Date(Number(h.run_at)),
|
|
130
|
+
createdAt: new Date(Number(h.created_at)),
|
|
131
|
+
updatedAt: new Date(Number(h.updated_at)),
|
|
132
|
+
};
|
|
133
|
+
if (h.last_error !== undefined)
|
|
134
|
+
record.lastError = h.last_error;
|
|
135
|
+
if (h.traceparent !== undefined) {
|
|
136
|
+
record.trace = { traceparent: h.traceparent };
|
|
137
|
+
if (h.tracestate !== undefined)
|
|
138
|
+
record.trace.tracestate = h.tracestate;
|
|
139
|
+
}
|
|
140
|
+
if (h.tenant_id !== undefined)
|
|
141
|
+
record.tenantId = h.tenant_id;
|
|
142
|
+
return record;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Jobs in Redis, shared by every instance, with the guarantees of the database queue: each due job is claimed by
|
|
146
|
+
* one worker (one atomic script), failed attempts are retried with backoff, a running job whose worker stopped
|
|
147
|
+
* reporting for staleAfterMs is claimed again (or failed without attempts left), and cron ticks are queued once.
|
|
148
|
+
*/
|
|
149
|
+
export function redisJobStore(client, prefix, options = {}) {
|
|
150
|
+
const staleAfterMs = options.staleAfterMs ?? STALE_AFTER_MS;
|
|
151
|
+
const finishedTtlMs = options.finishedTtlMs ?? REDIS_FINISHED_TTL_MS;
|
|
152
|
+
const jobPrefix = `${prefix}jobs:job:`;
|
|
153
|
+
const duePrefix = `${prefix}jobs:due:`;
|
|
154
|
+
const running = `${prefix}jobs:running`;
|
|
155
|
+
return {
|
|
156
|
+
async add(r) {
|
|
157
|
+
const pairs = [
|
|
158
|
+
"id", r.id, "name", r.name, "input", JSON.stringify(r.input ?? null), "status", r.status, "attempts", String(r.attempts),
|
|
159
|
+
"max_attempts", String(r.maxAttempts), "run_at", String(r.runAt.getTime()), "created_at", String(r.createdAt.getTime()),
|
|
160
|
+
"updated_at", String(r.updatedAt.getTime()),
|
|
161
|
+
];
|
|
162
|
+
if (r.lastError !== undefined)
|
|
163
|
+
pairs.push("last_error", r.lastError);
|
|
164
|
+
if (r.trace?.traceparent)
|
|
165
|
+
pairs.push("traceparent", r.trace.traceparent);
|
|
166
|
+
if (r.trace?.tracestate)
|
|
167
|
+
pairs.push("tracestate", r.trace.tracestate);
|
|
168
|
+
if (r.tenantId)
|
|
169
|
+
pairs.push("tenant_id", r.tenantId);
|
|
170
|
+
await client.sendCommand(["EVAL", ADD, "2", jobPrefix + r.id, duePrefix + r.name, r.id, String(r.runAt.getTime()), r.status, ...pairs]);
|
|
171
|
+
},
|
|
172
|
+
async claim(now, limit, names) {
|
|
173
|
+
if (!names.length || limit < 1)
|
|
174
|
+
return [];
|
|
175
|
+
const reply = (await client.sendCommand([
|
|
176
|
+
"EVAL", CLAIM, String(names.length + 1), running, ...names.map((n) => duePrefix + n),
|
|
177
|
+
String(now.getTime()), String(limit), String(staleAfterMs), STOPPED_MESSAGE, jobPrefix, String(finishedTtlMs), ...names,
|
|
178
|
+
]));
|
|
179
|
+
return reply.map((raw) => {
|
|
180
|
+
const h = fields(raw);
|
|
181
|
+
return fromHash(h.id ?? "", h);
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
async update(id, patch) {
|
|
185
|
+
const pairs = [];
|
|
186
|
+
if (patch.status !== undefined)
|
|
187
|
+
pairs.push("status", patch.status);
|
|
188
|
+
if (patch.runAt !== undefined)
|
|
189
|
+
pairs.push("run_at", String(patch.runAt.getTime()));
|
|
190
|
+
if (patch.lastError !== undefined)
|
|
191
|
+
pairs.push("last_error", patch.lastError);
|
|
192
|
+
await client.sendCommand(["EVAL", UPDATE, "2", jobPrefix + id, running, id, duePrefix, String(finishedTtlMs), ...pairs]);
|
|
193
|
+
},
|
|
194
|
+
async get(id) {
|
|
195
|
+
const h = fields(await client.sendCommand(["HGETALL", jobPrefix + id]));
|
|
196
|
+
return h.name === undefined ? null : fromHash(id, h);
|
|
197
|
+
},
|
|
198
|
+
async touch(id) {
|
|
199
|
+
await client.sendCommand(["EVAL", TOUCH, "2", jobPrefix + id, running, id]);
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=redis-jobs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-jobs.js","sourceRoot":"","sources":["../src/redis-jobs.ts"],"names":[],"mappings":"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"}
|
package/dist/redis.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis for state shared by every instance: createApp({ redis: client, rateLimit: { store: "redis" }, cache: { store: "redis" } }).
|
|
3
|
+
* Inferlo does not depend on a Redis package. It talks to the client you create and connect (the official
|
|
4
|
+
* `redis` package, node-redis 5 or newer) through its sendCommand method, and never closes it.
|
|
5
|
+
*/
|
|
6
|
+
import { InferloError } from "./errors.ts";
|
|
7
|
+
import type { CacheStore } from "./cache.ts";
|
|
8
|
+
import type { RateLimitStore } from "./rate-limit.ts";
|
|
9
|
+
/**
|
|
10
|
+
* A connected Redis client: the client from createClient() of the `redis` package (await client.connect() first).
|
|
11
|
+
* For ioredis, pass { sendCommand: (args) => ioredis.call(args[0], ...args.slice(1)) }.
|
|
12
|
+
*/
|
|
13
|
+
export interface RedisClient {
|
|
14
|
+
sendCommand(args: string[]): Promise<unknown>;
|
|
15
|
+
}
|
|
16
|
+
/** Checks createApp({ redis }) at startup. Raises IL6031. */
|
|
17
|
+
export declare function checkRedisClient(client: unknown): RedisClient | undefined;
|
|
18
|
+
/** IL6031 for a store "redis" without createApp({ redis }). */
|
|
19
|
+
export declare function redisMissing(option: string): InferloError;
|
|
20
|
+
/** The key prefix for an app: "inferlo:<app name>:", so apps sharing one Redis keep their keys apart. */
|
|
21
|
+
export declare function redisPrefix(appName: string | undefined): string;
|
|
22
|
+
/** Readiness: PING answers. */
|
|
23
|
+
export declare function pingRedis(client: RedisClient): Promise<void>;
|
|
24
|
+
/** Rate limit counts in Redis: one key per client and route window, which Redis expires with the window. */
|
|
25
|
+
export declare function redisRateLimitStore(client: RedisClient, prefix: string): RateLimitStore;
|
|
26
|
+
/**
|
|
27
|
+
* Cache entries in Redis, shared by every instance: one key per entry, expired by Redis, and one set per tag
|
|
28
|
+
* with the keys stored under it, so cache.invalidate(tag) reaches every instance.
|
|
29
|
+
*/
|
|
30
|
+
export declare function redisCacheStore(client: RedisClient, prefix: string): CacheStore;
|
package/dist/redis.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis for state shared by every instance: createApp({ redis: client, rateLimit: { store: "redis" }, cache: { store: "redis" } }).
|
|
3
|
+
* Inferlo does not depend on a Redis package. It talks to the client you create and connect (the official
|
|
4
|
+
* `redis` package, node-redis 5 or newer) through its sendCommand method, and never closes it.
|
|
5
|
+
*/
|
|
6
|
+
import { InferloError } from "./errors.js";
|
|
7
|
+
const fix = 'Create and connect a client with the redis package: const redis = await createClient({ url: process.env.REDIS_URL }).connect(); then createApp({ redis, rateLimit: { store: "redis" } }).';
|
|
8
|
+
/** Checks createApp({ redis }) at startup. Raises IL6031. */
|
|
9
|
+
export function checkRedisClient(client) {
|
|
10
|
+
if (client === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (typeof client !== "object" || client === null || typeof client.sendCommand !== "function") {
|
|
13
|
+
throw new InferloError("IL6031", "createApp({ redis }) needs a Redis client with a sendCommand method.", { fix });
|
|
14
|
+
}
|
|
15
|
+
return client;
|
|
16
|
+
}
|
|
17
|
+
/** IL6031 for a store "redis" without createApp({ redis }). */
|
|
18
|
+
export function redisMissing(option) {
|
|
19
|
+
return new InferloError("IL6031", `${option} store "redis" needs a Redis client, but createApp has no redis option.`, { fix });
|
|
20
|
+
}
|
|
21
|
+
/** The key prefix for an app: "inferlo:<app name>:", so apps sharing one Redis keep their keys apart. */
|
|
22
|
+
export function redisPrefix(appName) {
|
|
23
|
+
return `inferlo:${appName ?? "inferlo-app"}:`;
|
|
24
|
+
}
|
|
25
|
+
/** Readiness: PING answers. */
|
|
26
|
+
export async function pingRedis(client) {
|
|
27
|
+
const reply = await client.sendCommand(["PING"]);
|
|
28
|
+
if (String(reply) !== "PONG")
|
|
29
|
+
throw new Error(`Redis answered PING with ${JSON.stringify(reply)}.`);
|
|
30
|
+
}
|
|
31
|
+
// One atomic step per hit: count, start the window on the first hit, and report the time left.
|
|
32
|
+
// The PTTL check repairs a key that lost its expiry, so a window can never last forever.
|
|
33
|
+
const HIT = `local count = redis.call("INCR", KEYS[1])
|
|
34
|
+
if count == 1 then redis.call("PEXPIRE", KEYS[1], ARGV[1]) end
|
|
35
|
+
local ttl = redis.call("PTTL", KEYS[1])
|
|
36
|
+
if ttl < 0 then redis.call("PEXPIRE", KEYS[1], ARGV[1]) ttl = tonumber(ARGV[1]) end
|
|
37
|
+
return { count, ttl }`;
|
|
38
|
+
/** Rate limit counts in Redis: one key per client and route window, which Redis expires with the window. */
|
|
39
|
+
export function redisRateLimitStore(client, prefix) {
|
|
40
|
+
return {
|
|
41
|
+
async hit(key, ms) {
|
|
42
|
+
const reply = (await client.sendCommand(["EVAL", HIT, "1", `${prefix}ratelimit:${key}`, String(ms)]));
|
|
43
|
+
return { count: Number(reply[0]), resetMs: Math.max(0, Number(reply[1])) };
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Stores an entry with its time to live and adds it to each tag's set. A tag set lives as long as its
|
|
48
|
+
// longest-lived entry, so sets of expired entries are removed by Redis too.
|
|
49
|
+
const SET_ENTRY = `redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
|
|
50
|
+
local ttl = tonumber(ARGV[2])
|
|
51
|
+
for i = 2, #KEYS do
|
|
52
|
+
redis.call("SADD", KEYS[i], KEYS[1])
|
|
53
|
+
if redis.call("PTTL", KEYS[i]) < ttl then redis.call("PEXPIRE", KEYS[i], ttl) end
|
|
54
|
+
end
|
|
55
|
+
return 1`;
|
|
56
|
+
// Removes every entry in the given tag sets, then the sets.
|
|
57
|
+
const INVALIDATE = `for i = 1, #KEYS do
|
|
58
|
+
local members = redis.call("SMEMBERS", KEYS[i])
|
|
59
|
+
for j = 1, #members, 500 do
|
|
60
|
+
redis.call("DEL", unpack(members, j, math.min(j + 499, #members)))
|
|
61
|
+
end
|
|
62
|
+
redis.call("DEL", KEYS[i])
|
|
63
|
+
end
|
|
64
|
+
return 1`;
|
|
65
|
+
/**
|
|
66
|
+
* Cache entries in Redis, shared by every instance: one key per entry, expired by Redis, and one set per tag
|
|
67
|
+
* with the keys stored under it, so cache.invalidate(tag) reaches every instance.
|
|
68
|
+
*/
|
|
69
|
+
export function redisCacheStore(client, prefix) {
|
|
70
|
+
const entry = (key) => `${prefix}cache:entry:${key}`;
|
|
71
|
+
const tag = (name) => `${prefix}cache:tag:${name}`;
|
|
72
|
+
return {
|
|
73
|
+
async get(key) {
|
|
74
|
+
const value = await client.sendCommand(["GET", entry(key)]);
|
|
75
|
+
return value === null || value === undefined ? undefined : String(value);
|
|
76
|
+
},
|
|
77
|
+
async set(key, json, ttlMs, tags) {
|
|
78
|
+
const keys = [entry(key), ...tags.map(tag)];
|
|
79
|
+
await client.sendCommand(["EVAL", SET_ENTRY, String(keys.length), ...keys, json, String(ttlMs)]);
|
|
80
|
+
},
|
|
81
|
+
async delete(key) {
|
|
82
|
+
await client.sendCommand(["DEL", entry(key)]);
|
|
83
|
+
},
|
|
84
|
+
async invalidate(tags) {
|
|
85
|
+
if (!tags.length)
|
|
86
|
+
return;
|
|
87
|
+
await client.sendCommand(["EVAL", INVALIDATE, String(tags.length), ...tags.map(tag)]);
|
|
88
|
+
},
|
|
89
|
+
async clear() {
|
|
90
|
+
// SCAN walks the keys without blocking Redis; only this app's cache keys match the pattern.
|
|
91
|
+
const pattern = `${escapeGlob(prefix)}cache:*`;
|
|
92
|
+
let cursor = "0";
|
|
93
|
+
do {
|
|
94
|
+
const [next, keys] = (await client.sendCommand(["SCAN", cursor, "MATCH", pattern, "COUNT", "500"]));
|
|
95
|
+
cursor = String(next);
|
|
96
|
+
if (keys.length)
|
|
97
|
+
await client.sendCommand(["DEL", ...keys.map(String)]);
|
|
98
|
+
} while (cursor !== "0");
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Escapes the characters SCAN MATCH treats as a pattern, so an app name like "a*b" matches only itself. */
|
|
103
|
+
function escapeGlob(text) {
|
|
104
|
+
return text.replace(/[*?[\]\\]/g, "\\$&");
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=redis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../src/redis.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text of JSON responses Inferlo builds itself, so a Node.js server can send it in one write with a
|
|
3
|
+
* Content-Length instead of reading the Response body stream chunk by chunk. A response that is copied, or
|
|
4
|
+
* whose body was read, is not in the map and is streamed as usual.
|
|
5
|
+
*/
|
|
6
|
+
export declare const knownBodies: WeakMap<Response, string>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text of JSON responses Inferlo builds itself, so a Node.js server can send it in one write with a
|
|
3
|
+
* Content-Length instead of reading the Response body stream chunk by chunk. A response that is copied, or
|
|
4
|
+
* whose body was read, is not in the map and is streamed as usual.
|
|
5
|
+
*/
|
|
6
|
+
export const knownBodies = new WeakMap();
|
|
7
|
+
//# sourceMappingURL=response-body.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-body.js","sourceRoot":"","sources":["../src/response-body.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,OAAO,EAAoB,CAAC"}
|
package/dist/route.d.ts
CHANGED
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
import type { Deprecation } from "./deprecation.ts";
|
|
12
12
|
import type { AI } from "./ai/index.ts";
|
|
13
13
|
import type { Db } from "./db/table.ts";
|
|
14
|
+
import type { Events } from "./events.ts";
|
|
14
15
|
import type { Jobs } from "./jobs.ts";
|
|
15
16
|
import type { AnyModel } from "./model.ts";
|
|
16
17
|
import type { Audit } from "./audit.ts";
|
|
17
18
|
import type { Cache, RouteCache } from "./cache.ts";
|
|
18
19
|
import type { PermissionInput, Permissions } from "./permissions.ts";
|
|
19
20
|
import type { RateLimit } from "./rate-limit.ts";
|
|
20
|
-
import type { AnySchema, Infer, ObjectSchema } from "./schema.ts";
|
|
21
|
+
import type { AnySchema, Infer, InferInput, ObjectSchema } from "./schema.ts";
|
|
21
22
|
/**
|
|
22
23
|
* Register your app's models and user type once, so every handler is fully typed:
|
|
23
24
|
*
|
|
@@ -112,6 +113,8 @@ export interface Context<Body, Query, Params, User> {
|
|
|
112
113
|
ai: AI;
|
|
113
114
|
/** Queue background jobs: await jobs.enqueue(sendEmail, { to }). */
|
|
114
115
|
jobs: Jobs;
|
|
116
|
+
/** Publish events: await events.publish(orderPaid, { orderId }) queues the jobs subscribed with on(event, job). */
|
|
117
|
+
events: Events;
|
|
115
118
|
/** The app cache: await cache.getOrSet(key, { ttlMs: 60_000, tags: ["tickets"] }, compute), cache.invalidate("tickets"). */
|
|
116
119
|
cache: Cache;
|
|
117
120
|
req: Request;
|
|
@@ -139,6 +142,15 @@ export interface Context<Body, Query, Params, User> {
|
|
|
139
142
|
audit: Audit;
|
|
140
143
|
}
|
|
141
144
|
export type AnyContext = Context<any, any, any, any>;
|
|
145
|
+
/**
|
|
146
|
+
* What a caller sends to a route, before validation (defaults not yet applied): the body, query and path params.
|
|
147
|
+
* Type only ($input on the route returned by handle()), used by serviceClient for typed calls.
|
|
148
|
+
*/
|
|
149
|
+
export interface RouteInput<Path extends string, B, Q, P> {
|
|
150
|
+
body: B extends AnySchema ? InferInput<B> : undefined;
|
|
151
|
+
query: Q extends AnySchema ? InferInput<Q> : Record<string, string | undefined>;
|
|
152
|
+
params: P extends AnySchema ? InferInput<P> : PathParams<Path>;
|
|
153
|
+
}
|
|
142
154
|
export type Handler<C, R> = (ctx: C) => R | Promise<R>;
|
|
143
155
|
export interface RouteDef {
|
|
144
156
|
readonly kind: "inferlo.route";
|
|
@@ -159,6 +171,7 @@ export declare class RouteBuilder<Path extends string, B extends AnySchema | und
|
|
|
159
171
|
/** The request handler. Return data (sent as JSON), a Response, or nothing (204). */
|
|
160
172
|
handle<R>(handler: Handler<Context<B extends AnySchema ? Infer<B> : undefined, Q extends AnySchema ? Infer<Q> : Record<string, string | undefined>, P extends AnySchema ? Infer<P> : PathParams<Path>, UserFor<A>>, R>): RouteDef & {
|
|
161
173
|
readonly $response: Awaited<R>;
|
|
174
|
+
readonly $input: RouteInput<Path, B, Q, P>;
|
|
162
175
|
};
|
|
163
176
|
}
|
|
164
177
|
export declare const route: {
|
package/dist/route.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route.js","sourceRoot":"","sources":["../src/route.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"route.js","sourceRoot":"","sources":["../src/route.ts"],"names":[],"mappings":"AA2KA,MAAM,OAAO,YAAY;IACd,MAAM,CAAS;IACf,IAAI,CAAO;IACX,OAAO,CAAwB;IAExC,YAAY,MAAc,EAAE,IAAU,EAAE,OAA8B;QACpE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,qFAAqF;IACrF,MAAM,CACJ,OAQC;QAED,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,OAAuC,EAG5I,CAAC;IACJ,CAAC;CACF;AAED,SAAS,OAAO,CAAC,MAAc;IAC7B,OAAO,CAOL,IAAU,EACV,UAAgD,EAAE,EAClD,EAAE,CAAC,IAAI,YAAY,CAAmB,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC;IACrB,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC;CAC1B,CAAC;AAEF,MAAM,UAAU,OAAO,CAAC,KAAc;IACpC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAK,KAAkB,CAAC,IAAI,KAAK,eAAe,CAAC;AACrG,CAAC;AAED,gHAAgH;AAChH,MAAM,UAAU,aAAa,CAAC,OAAgB;IAC5C,MAAM,GAAG,GAAe,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAW,CAAC;IAChC,MAAM,KAAK,GAAG,CAAC,MAAe,EAAE,IAAa,EAAE,EAAE;QAC/C,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,OAAO;QACtE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACjB,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACpB,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YAC9D,OAAO;QACT,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC1B,KAAK,MAAM,IAAI,IAAI,MAAM;gBAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YACvC,OAAO;QACT,CAAC;QACD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;YAAE,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;gBAAE,KAAK,CAAC,KAAK,EAAE,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACnJ,CAAC,CAAC;IACF,KAAK,CAAC,OAAO,CAAC,CAAC;IACf,OAAO,GAAG,CAAC;AACb,CAAC"}
|