@browserlessapi/inferlo 0.6.0 → 0.8.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 +9 -5
- 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 +55 -4
- package/dist/app.js +439 -39
- 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/authjs.d.ts +70 -0
- package/dist/authjs.js +94 -0
- package/dist/authjs.js.map +1 -0
- package/dist/azure.d.ts +66 -0
- package/dist/azure.js +119 -0
- package/dist/azure.js.map +1 -0
- package/dist/better-auth.d.ts +89 -0
- package/dist/better-auth.js +75 -0
- package/dist/better-auth.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 +5 -5
- package/dist/cli/main.js.map +1 -1
- package/dist/client-ip.d.ts +41 -1
- package/dist/client-ip.js +85 -1
- package/dist/client-ip.js.map +1 -1
- package/dist/cross-site.d.ts +14 -0
- package/dist/cross-site.js +56 -0
- package/dist/cross-site.js.map +1 -0
- package/dist/db/memory.js +2 -1
- package/dist/db/memory.js.map +1 -1
- package/dist/db/postgres.js +23 -6
- 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 +23 -5
- package/dist/describe.js +20 -2
- package/dist/describe.js.map +1 -1
- package/dist/disk-storage.d.ts +6 -0
- package/dist/disk-storage.js +96 -0
- package/dist/disk-storage.js.map +1 -0
- package/dist/events.d.ts +134 -0
- package/dist/events.js +196 -0
- package/dist/events.js.map +1 -0
- package/dist/files.d.ts +23 -0
- package/dist/files.js +168 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +26 -8
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/jobs.d.ts +56 -10
- package/dist/jobs.js +71 -13
- package/dist/jobs.js.map +1 -1
- package/dist/jwt.d.ts +83 -0
- package/dist/jwt.js +312 -0
- package/dist/jwt.js.map +1 -0
- 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 +41 -7
- package/dist/model.js.map +1 -1
- package/dist/oidc.d.ts +70 -0
- package/dist/oidc.js +379 -0
- package/dist/oidc.js.map +1 -0
- package/dist/openapi.js +17 -2
- package/dist/openapi.js.map +1 -1
- package/dist/passport.d.ts +54 -0
- package/dist/passport.js +68 -0
- package/dist/passport.js.map +1 -0
- package/dist/postgres-js.d.ts +39 -0
- package/dist/postgres-js.js +71 -0
- package/dist/postgres-js.js.map +1 -0
- 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-sockets.d.ts +24 -0
- package/dist/redis-sockets.js +75 -0
- package/dist/redis-sockets.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 +117 -3
- package/dist/route.js +27 -0
- package/dist/route.js.map +1 -1
- package/dist/schema.d.ts +23 -3
- package/dist/schema.js +70 -0
- package/dist/schema.js.map +1 -1
- package/dist/serve.d.ts +24 -1
- package/dist/serve.js +200 -8
- 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 +36 -4
- package/dist/sessions.js.map +1 -1
- package/dist/sockets.d.ts +58 -0
- package/dist/sockets.js +111 -0
- package/dist/sockets.js.map +1 -0
- package/dist/storage.d.ts +47 -0
- package/dist/storage.js +123 -0
- package/dist/storage.js.map +1 -0
- package/dist/tracing.d.ts +7 -3
- package/dist/tracing.js +8 -2
- package/dist/tracing.js.map +1 -1
- package/dist/websocket.d.ts +81 -0
- package/dist/websocket.js +251 -0
- package/dist/websocket.js.map +1 -0
- package/llms.txt +2 -0
- package/package.json +77 -5
- package/templates/app/AGENTS.md +9 -5
- package/templates/app/test/notes.test.ts +2 -0
|
@@ -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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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { RedisClient } from "./redis.ts";
|
|
2
|
+
import type { SocketRelay } from "./sockets.ts";
|
|
3
|
+
/**
|
|
4
|
+
* A second, connected Redis client for receiving broadcasts: a client in subscriber mode cannot send other
|
|
5
|
+
* commands, so it cannot be the createApp({ redis }) client. With the redis package: await redis.duplicate().connect().
|
|
6
|
+
* For ioredis, pass { subscribe: async (channel, listener) => { sub.on("message", (c, m) => c === channel && listener(m)); await sub.subscribe(channel); }, unsubscribe: (channel) => sub.unsubscribe(channel) }.
|
|
7
|
+
*/
|
|
8
|
+
export interface RedisSubscriber {
|
|
9
|
+
subscribe(channel: string, listener: (message: string, channel: string) => void): Promise<unknown>;
|
|
10
|
+
unsubscribe(channel: string): Promise<unknown>;
|
|
11
|
+
}
|
|
12
|
+
export interface SocketsConfig {
|
|
13
|
+
/**
|
|
14
|
+
* How broadcasts reach the sockets of other instances. Default: none, sockets.broadcast reaches the sockets of this
|
|
15
|
+
* process only. "redis": Redis Pub/Sub in the Redis of createApp({ redis }) (needs name and subscriber), so a
|
|
16
|
+
* broadcast from any instance or from inferlo worker reaches every instance's sockets in the room.
|
|
17
|
+
*/
|
|
18
|
+
transport?: "redis";
|
|
19
|
+
/** With transport "redis": a second connected Redis client that receives broadcasts (await redis.duplicate().connect()). */
|
|
20
|
+
subscriber?: RedisSubscriber;
|
|
21
|
+
}
|
|
22
|
+
/** Checks createApp({ sockets }) at startup and returns the subscriber for transport "redis". Raises IL6036. */
|
|
23
|
+
export declare function checkSocketsConfig(config: unknown, redis: RedisClient | undefined, name: string | undefined): RedisSubscriber | undefined;
|
|
24
|
+
export declare function redisSocketRelay(client: RedisClient, subscriber: RedisSubscriber, channel: string, onError: (message: string, error?: unknown) => void): SocketRelay;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket broadcasts across instances over Redis Pub/Sub:
|
|
3
|
+
* createApp({ name: "boards", redis, sockets: { transport: "redis", subscriber: await redis.duplicate().connect() } }).
|
|
4
|
+
*
|
|
5
|
+
* Every broadcast is sent to the sockets of this process and published on one channel per app,
|
|
6
|
+
* inferlo:<app name>:sockets, as JSON { o: instance id, k: room key (with the tenant), d: message text }. Each
|
|
7
|
+
* instance with open sockets subscribes to the channel (on its first WebSocket connection) and sends what other
|
|
8
|
+
* instances published to its own sockets in that room. Pub/Sub keeps nothing: an instance that is not subscribed
|
|
9
|
+
* when a broadcast is published does not get it, as with sockets that join a room after a broadcast.
|
|
10
|
+
*/
|
|
11
|
+
import { InferloError } from "./errors.js";
|
|
12
|
+
const FIX = 'Use createApp({ name: "boards", redis, sockets: { transport: "redis", subscriber: await redis.duplicate().connect() } }), with redis = await createClient({ url: process.env.REDIS_URL }).connect().';
|
|
13
|
+
function invalid(message) {
|
|
14
|
+
return new InferloError("IL6036", message, { fix: FIX });
|
|
15
|
+
}
|
|
16
|
+
/** Checks createApp({ sockets }) at startup and returns the subscriber for transport "redis". Raises IL6036. */
|
|
17
|
+
export function checkSocketsConfig(config, redis, name) {
|
|
18
|
+
if (config === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (typeof config !== "object" || config === null || Array.isArray(config))
|
|
21
|
+
throw invalid("createApp({ sockets }) must be an object.");
|
|
22
|
+
const { transport, subscriber } = config;
|
|
23
|
+
const unknown = Object.keys(config).filter((key) => key !== "transport" && key !== "subscriber");
|
|
24
|
+
if (unknown.length)
|
|
25
|
+
throw invalid(`Unknown sockets option ${unknown.map((key) => `"${key}"`).join(", ")}.`);
|
|
26
|
+
if (transport === undefined) {
|
|
27
|
+
if (subscriber !== undefined)
|
|
28
|
+
throw invalid('sockets.subscriber is only used with transport "redis".');
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
if (transport !== "redis")
|
|
32
|
+
throw invalid(`Invalid sockets transport ${JSON.stringify(transport)}: the only transport is "redis".`);
|
|
33
|
+
if (!redis)
|
|
34
|
+
throw invalid('sockets transport "redis" needs a Redis client, but createApp has no redis option.');
|
|
35
|
+
// Instances find each other by the channel named after the app, so apps sharing one Redis must have names.
|
|
36
|
+
if (!name)
|
|
37
|
+
throw invalid('sockets transport "redis" needs createApp({ name }): the instances of an app share one channel, named after the app.');
|
|
38
|
+
if (typeof subscriber !== "object" || subscriber === null || typeof subscriber.subscribe !== "function" || typeof subscriber.unsubscribe !== "function") {
|
|
39
|
+
throw invalid('sockets transport "redis" needs a subscriber: a second connected Redis client with subscribe and unsubscribe.');
|
|
40
|
+
}
|
|
41
|
+
if (subscriber === redis)
|
|
42
|
+
throw invalid("sockets.subscriber must be a second Redis client, not the createApp({ redis }) client: a subscribed client cannot send other commands.");
|
|
43
|
+
return subscriber;
|
|
44
|
+
}
|
|
45
|
+
export function redisSocketRelay(client, subscriber, channel, onError) {
|
|
46
|
+
const instance = crypto.randomUUID();
|
|
47
|
+
let listener;
|
|
48
|
+
return {
|
|
49
|
+
async publish(key, text) {
|
|
50
|
+
await client.sendCommand(["PUBLISH", channel, JSON.stringify({ o: instance, k: key, d: text })]);
|
|
51
|
+
},
|
|
52
|
+
async start(deliver) {
|
|
53
|
+
listener = (message) => {
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(String(message));
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return onError(`ignored a message on ${channel} that is not JSON`, error);
|
|
60
|
+
}
|
|
61
|
+
if (parsed.o === instance)
|
|
62
|
+
return;
|
|
63
|
+
if (typeof parsed.k !== "string" || typeof parsed.d !== "string")
|
|
64
|
+
return onError(`ignored a message on ${channel} without a room and data`);
|
|
65
|
+
deliver(parsed.k, parsed.d);
|
|
66
|
+
};
|
|
67
|
+
await subscriber.subscribe(channel, (message) => listener?.(message));
|
|
68
|
+
},
|
|
69
|
+
async stop() {
|
|
70
|
+
listener = undefined;
|
|
71
|
+
await subscriber.unsubscribe(channel);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=redis-sockets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-sockets.js","sourceRoot":"","sources":["../src/redis-sockets.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"}
|