@getstrata/core 2.0.9 → 2.2.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/CHANGELOG.md +37 -0
- package/dist/core/cache/createCacheStore.d.ts +3 -1
- package/dist/core/cache/redisCacheStore.d.ts +21 -8
- package/dist/core/cache/repository.d.ts +3 -2
- package/dist/core/cache/store.d.ts +4 -1
- package/dist/core/cache/taggedCache.d.ts +1 -1
- package/dist/core/contracts/container.d.ts +28 -10
- package/dist/core/contracts/di.d.ts +7 -4
- package/dist/core/contracts/serviceTokens.d.ts +6 -1
- package/dist/core/database/baseRepository.d.ts +0 -1
- package/dist/core/database/index.d.ts +1 -1
- package/dist/core/database/model.d.ts +123 -42
- package/dist/core/database/repositoryQuery.d.ts +16 -1
- package/dist/core/database/types.d.ts +25 -2
- package/dist/core/database/whereBuilder.d.ts +5 -5
- package/dist/core/events/deferredModelEvents.d.ts +2 -1
- package/dist/core/http/loginThrottleMiddleware.d.ts +6 -5
- package/dist/core/http/memoryThrottleMiddleware.d.ts +11 -3
- package/dist/core/http/middleware.d.ts +2 -2
- package/dist/core/http/response.d.ts +2 -0
- package/dist/core/http/routeMiddleware.d.ts +3 -2
- package/dist/core/http/scimThrottleMiddleware.d.ts +12 -3
- package/dist/core/http/throttleMiddleware.d.ts +24 -6
- package/dist/core/lifecycle/gracefulShutdown.d.ts +33 -5
- package/dist/core/queue/index.d.ts +7 -0
- package/dist/core/queue/redisQueue.d.ts +1 -0
- package/dist/core/queue/resilientQueue.d.ts +2 -2
- package/dist/core/runtime/applicationRegistry.d.ts +2 -1
- package/dist/core/runtime/boundedThrottleStore.d.ts +29 -0
- package/dist/core/scheduler/leases.d.ts +40 -0
- package/dist/core/scheduler/schedule.d.ts +29 -5
- package/dist/core/tenant/tenantContext.d.ts +5 -4
- package/dist/core/tenant/tenantMiddleware.d.ts +7 -1
- package/dist/core/tracing/otel.d.ts +28 -26
- package/dist/core/tracing/tracingMiddleware.d.ts +4 -1
- package/dist/entries/audit/exportAuditLogs.js +14 -2
- package/dist/entries/auth/basicAuthGuard.js +80 -0
- package/dist/entries/auth/jwtGuard.js +80 -0
- package/dist/entries/auth/scimAuthMiddleware.js +17 -7
- package/dist/entries/auth/sessionGuard.js +80 -0
- package/dist/entries/cache/createCacheStore.js +365 -119
- package/dist/entries/cache/repository.js +5 -0
- package/dist/entries/contracts/container.js +8 -1
- package/dist/entries/contracts/serviceTokens.js +83 -0
- package/dist/entries/database/factory.js +0 -3
- package/dist/entries/database/model.js +1 -2465
- package/dist/entries/database/repositoryQuery.js +67 -9
- package/dist/entries/http/bodySizeLimitMiddleware.js +76 -10
- package/dist/entries/http/memoryThrottleMiddleware.js +276 -21
- package/dist/entries/http/routeMiddleware.js +1 -4
- package/dist/entries/http/scimThrottleMiddleware.js +576 -54
- package/dist/entries/http/throttleMiddleware.js +65 -16
- package/dist/entries/jobs/exportAuditLogsJob.js +44 -7
- package/dist/entries/jobs/invalidateCacheTagsJob.js +30 -5
- package/dist/entries/queue/createAppQueue.js +67 -6
- package/dist/entries/queue/publicQueue.js +67 -6
- package/dist/entries/queue/queueMetrics.js +67 -6
- package/dist/entries/queue/redisQueue.js +3 -0
- package/dist/entries/queue.js +30 -5
- package/dist/entries/scheduler/schedule.js +1 -72
- package/dist/entries/tenant/databaseTenantContext.js +14 -2
- package/dist/entries/tenant/tenantDatabaseScope.js +14 -2
- package/dist/entries/tracing/tracingMiddleware.js +186 -454
- package/dist/framework/public-api.d.ts +14 -10
- package/dist/index.js +1770 -638
- package/dist/types/services.d.ts +3 -3
- package/package.json +17 -8
|
@@ -89,151 +89,397 @@ function sdkClientClassName() {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// ../../src/core/cache/redisCacheStore.ts
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return
|
|
97
|
-
|
|
92
|
+
var CACHE_SCRIPT = `
|
|
93
|
+
local expected = {'hash', 'zset', 'zset', 'hash', 'zset'}
|
|
94
|
+
for i, key in ipairs(KEYS) do
|
|
95
|
+
local kind = redis.call('TYPE', key).ok
|
|
96
|
+
if kind ~= 'none' and kind ~= expected[i] then return redis.error_reply('Unexpected cache key type') end
|
|
97
|
+
end
|
|
98
|
+
-- Redis eviction can remove one container without its companions. Never read
|
|
99
|
+
-- orphaned values or leases after losing the expiry/order/generation indexes.
|
|
100
|
+
local entries = redis.call('HLEN', KEYS[1])
|
|
101
|
+
local order = redis.call('ZCARD', KEYS[3])
|
|
102
|
+
if redis.call('ZCARD', KEYS[2]) ~= order or entries ~= (order > 0 and order + 1 or 0) then
|
|
103
|
+
redis.call('DEL', KEYS[1], KEYS[2], KEYS[3])
|
|
104
|
+
end
|
|
105
|
+
if redis.call('HLEN', KEYS[4]) ~= redis.call('ZCARD', KEYS[5]) then
|
|
106
|
+
redis.call('DEL', KEYS[4], KEYS[5])
|
|
107
|
+
end
|
|
108
|
+
local r = cjson.decode(ARGV[1])
|
|
109
|
+
local time = redis.call('TIME')
|
|
110
|
+
local now = tonumber(time[1]) * 1000 + tonumber(time[2]) / 1000
|
|
111
|
+
local function removeEntry(key)
|
|
112
|
+
local removed = redis.call('HDEL', KEYS[1], 'e:' .. key)
|
|
113
|
+
redis.call('ZREM', KEYS[2], key)
|
|
114
|
+
redis.call('ZREM', KEYS[3], key)
|
|
115
|
+
return removed
|
|
116
|
+
end
|
|
117
|
+
local function removeMeta(field)
|
|
118
|
+
redis.call('HDEL', KEYS[4], field)
|
|
119
|
+
redis.call('ZREM', KEYS[5], field)
|
|
120
|
+
end
|
|
121
|
+
for _, key in ipairs(redis.call('ZRANGEBYSCORE', KEYS[2], '-inf', now, 'LIMIT', 0, r.capacity)) do removeEntry(key) end
|
|
122
|
+
for _, field in ipairs(redis.call('ZRANGEBYSCORE', KEYS[5], '-inf', now, 'LIMIT', 0, r.metadataLimit)) do removeMeta(field) end
|
|
123
|
+
local function expireContainers()
|
|
124
|
+
local last = redis.call('ZREVRANGE', KEYS[2], 0, 0, 'WITHSCORES')
|
|
125
|
+
if #last == 0 then
|
|
126
|
+
redis.call('DEL', KEYS[1], KEYS[2], KEYS[3])
|
|
127
|
+
elseif last[2] == 'inf' then
|
|
128
|
+
for i = 1, 3 do redis.call('PERSIST', KEYS[i]) end
|
|
129
|
+
else
|
|
130
|
+
local ttl = math.max(1, math.ceil(tonumber(last[2]) - now))
|
|
131
|
+
for i = 1, 3 do redis.call('PEXPIRE', KEYS[i], ttl) end
|
|
132
|
+
end
|
|
133
|
+
for i = 4, 5 do redis.call('PEXPIRE', KEYS[i], r.metadataTtlMs) end
|
|
134
|
+
end
|
|
135
|
+
local function response(value)
|
|
136
|
+
expireContainers()
|
|
137
|
+
return cjson.encode(value)
|
|
138
|
+
end
|
|
139
|
+
local function addTags(existing, added)
|
|
140
|
+
local seen = {}
|
|
141
|
+
local result = {}
|
|
142
|
+
for _, tags in ipairs({existing, added}) do
|
|
143
|
+
for _, tag in ipairs(tags) do
|
|
144
|
+
if not seen[tag] then seen[tag] = true; table.insert(result, tag) end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
if #result > 32 then error('Cache entries support at most 32 tags') end
|
|
148
|
+
return result
|
|
149
|
+
end
|
|
150
|
+
local function touch(key)
|
|
151
|
+
local sequence = redis.call('HINCRBY', KEYS[1], '_sequence', 1)
|
|
152
|
+
redis.call('ZADD', KEYS[3], sequence, key)
|
|
153
|
+
end
|
|
154
|
+
local function read(key, tags)
|
|
155
|
+
local raw = redis.call('HGET', KEYS[1], 'e:' .. key)
|
|
156
|
+
if not raw then return nil end
|
|
157
|
+
local entry = cjson.decode(raw)
|
|
158
|
+
if #tags > 0 then
|
|
159
|
+
entry.tags = addTags(entry.tags, tags)
|
|
160
|
+
redis.call('HSET', KEYS[1], 'e:' .. key, cjson.encode(entry))
|
|
161
|
+
end
|
|
162
|
+
touch(key)
|
|
163
|
+
return entry.payload
|
|
164
|
+
end
|
|
165
|
+
local function write(key, payload, tags)
|
|
166
|
+
local entry = {payload=payload, tags=tags}
|
|
167
|
+
redis.call('HSET', KEYS[1], 'e:' .. key, cjson.encode(entry))
|
|
168
|
+
redis.call('ZADD', KEYS[2], r.ttlMs == 0 and '+inf' or now + r.ttlMs, key)
|
|
169
|
+
touch(key)
|
|
170
|
+
local overflow = redis.call('ZCARD', KEYS[3]) - r.capacity
|
|
171
|
+
if overflow > 0 then
|
|
172
|
+
for _, victim in ipairs(redis.call('ZRANGE', KEYS[3], 0, overflow - 1)) do removeEntry(victim) end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
local leaseField = 'l:' .. r.key
|
|
176
|
+
local function lease()
|
|
177
|
+
local raw = redis.call('HGET', KEYS[4], leaseField)
|
|
178
|
+
if not raw then return nil end
|
|
179
|
+
local value = cjson.decode(raw)
|
|
180
|
+
if value.expires <= now or value.deadline <= now then removeMeta(leaseField); return nil end
|
|
181
|
+
return value
|
|
182
|
+
end
|
|
183
|
+
local function metadataSpace(count)
|
|
184
|
+
if redis.call('HLEN', KEYS[4]) + count > r.metadataLimit then
|
|
185
|
+
-- Pressure sheds generation/lease state. Every old owner is fenced; cached
|
|
186
|
+
-- entries remain valid because invalidation removes matching entries atomically.
|
|
187
|
+
redis.call('DEL', KEYS[4], KEYS[5])
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
local function putMeta(field, value, expires)
|
|
191
|
+
redis.call('HSET', KEYS[4], field, value)
|
|
192
|
+
redis.call('ZADD', KEYS[5], expires, field)
|
|
193
|
+
end
|
|
194
|
+
local function scopes()
|
|
195
|
+
local fields = {}
|
|
196
|
+
for _, tag in ipairs(r.tags) do table.insert(fields, 'g:tag:' .. tag) end
|
|
197
|
+
for _, prefix in ipairs(r.prefixes) do table.insert(fields, 'g:prefix:' .. prefix) end
|
|
198
|
+
return fields
|
|
199
|
+
end
|
|
200
|
+
local function matches(key, tags)
|
|
201
|
+
if r.op == 'invalidate' then return key == r.key end
|
|
202
|
+
if r.op == 'prefix' then return key == r.key or string.sub(key, 1, #r.key + 1) == r.key .. '?' end
|
|
203
|
+
if r.op == 'tags' then
|
|
204
|
+
for _, existing in ipairs(tags) do
|
|
205
|
+
for _, wanted in ipairs(r.tags) do if wanted == existing then return true end end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
return false
|
|
209
|
+
end
|
|
210
|
+
if r.op == 'clear' then
|
|
211
|
+
redis.call('DEL', unpack(KEYS))
|
|
212
|
+
return cjson.encode({removed=0})
|
|
213
|
+
elseif r.op == 'get' then
|
|
214
|
+
local payload = read(r.key, r.tags)
|
|
215
|
+
return response(payload and {kind='value', payload=payload} or {kind='missing'})
|
|
216
|
+
elseif r.op == 'set' then
|
|
217
|
+
local old = redis.call('HGET', KEYS[1], 'e:' .. r.key)
|
|
218
|
+
local tags = old and cjson.decode(old).tags or {}
|
|
219
|
+
write(r.key, r.payload, tags)
|
|
220
|
+
removeMeta(leaseField)
|
|
221
|
+
return response({written=true})
|
|
222
|
+
elseif r.op == 'attach' then
|
|
223
|
+
read(r.key, r.tags)
|
|
224
|
+
return response({written=true})
|
|
225
|
+
elseif r.op == 'size' then
|
|
226
|
+
return response({size=redis.call('ZCARD', KEYS[3])})
|
|
227
|
+
elseif r.op == 'tags' or r.op == 'prefix' or r.op == 'invalidate' then
|
|
228
|
+
local generationFields = {}
|
|
229
|
+
if r.op == 'tags' then
|
|
230
|
+
for _, tag in ipairs(r.tags) do table.insert(generationFields, 'g:tag:' .. tag) end
|
|
231
|
+
elseif r.op == 'prefix' then table.insert(generationFields, 'g:prefix:' .. r.key) end
|
|
232
|
+
metadataSpace(#generationFields)
|
|
233
|
+
for _, field in ipairs(generationFields) do putMeta(field, r.owner, now + r.metadataTtlMs) end
|
|
234
|
+
local removed = 0
|
|
235
|
+
for _, key in ipairs(redis.call('ZRANGE', KEYS[3], 0, -1)) do
|
|
236
|
+
local raw = redis.call('HGET', KEYS[1], 'e:' .. key)
|
|
237
|
+
if raw and matches(key, cjson.decode(raw).tags) then removed = removed + removeEntry(key) end
|
|
238
|
+
end
|
|
239
|
+
for _, field in ipairs(redis.call('HKEYS', KEYS[4])) do
|
|
240
|
+
if string.sub(field, 1, 2) == 'l:' then
|
|
241
|
+
local value = cjson.decode(redis.call('HGET', KEYS[4], field))
|
|
242
|
+
if matches(string.sub(field, 3), value.tags) then removeMeta(field) end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
return response({removed=removed})
|
|
246
|
+
elseif r.op == 'claim' then
|
|
247
|
+
local payload = read(r.key, r.tags)
|
|
248
|
+
if payload then return response({kind='value', payload=payload}) end
|
|
249
|
+
local current = lease()
|
|
250
|
+
if current then
|
|
251
|
+
local shared = addTags(current.tags, r.tags)
|
|
252
|
+
if #shared == #current.tags then return response({kind='wait'}) end
|
|
253
|
+
-- A newly requested tag must not inherit a fill started before that tag's
|
|
254
|
+
-- invalidation. Restart with the union of tags and a new owner.
|
|
255
|
+
r.tags = shared
|
|
256
|
+
removeMeta(leaseField)
|
|
257
|
+
end
|
|
258
|
+
local active = 0
|
|
259
|
+
for _, field in ipairs(redis.call('HKEYS', KEYS[4])) do if string.sub(field, 1, 2) == 'l:' then active = active + 1 end end
|
|
260
|
+
if active >= r.capacity then return response({kind='wait'}) end
|
|
261
|
+
local fields = scopes()
|
|
262
|
+
metadataSpace(#fields + 1)
|
|
263
|
+
local generations = {}
|
|
264
|
+
for _, field in ipairs(fields) do
|
|
265
|
+
local token = redis.call('HGET', KEYS[4], field) or r.owner
|
|
266
|
+
putMeta(field, token, now + r.metadataTtlMs)
|
|
267
|
+
generations[field] = token
|
|
268
|
+
end
|
|
269
|
+
local value = {owner=r.owner, tags=r.tags, generations=generations, deadline=now + r.fillTimeoutMs, expires=now + r.leaseMs}
|
|
270
|
+
putMeta(leaseField, cjson.encode(value), value.expires)
|
|
271
|
+
return response({kind='owner'})
|
|
272
|
+
elseif r.op == 'renew' or r.op == 'publish' or r.op == 'release' then
|
|
273
|
+
local current = lease()
|
|
274
|
+
if not current or current.owner ~= r.owner then return response({owned=false}) end
|
|
275
|
+
if r.op == 'release' then removeMeta(leaseField); return response({owned=true}) end
|
|
276
|
+
for field, token in pairs(current.generations) do
|
|
277
|
+
if redis.call('HGET', KEYS[4], field) ~= token then removeMeta(leaseField); return response({owned=false}) end
|
|
278
|
+
end
|
|
279
|
+
if r.op == 'renew' then
|
|
280
|
+
current.expires = math.min(current.deadline, now + r.leaseMs)
|
|
281
|
+
putMeta(leaseField, cjson.encode(current), current.expires)
|
|
282
|
+
else
|
|
283
|
+
write(r.key, r.payload, current.tags)
|
|
284
|
+
removeMeta(leaseField)
|
|
285
|
+
end
|
|
286
|
+
return response({owned=true})
|
|
287
|
+
end
|
|
288
|
+
return redis.error_reply('Unknown cache operation')
|
|
289
|
+
`;
|
|
98
290
|
|
|
99
291
|
class RedisCacheStore {
|
|
100
292
|
ttlMs;
|
|
101
293
|
maxEntries;
|
|
102
294
|
client;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
295
|
+
keys;
|
|
296
|
+
options;
|
|
297
|
+
fills = new Set;
|
|
298
|
+
closed = false;
|
|
299
|
+
constructor(redisUrl, ttlMs, maxEntries, options = {}) {
|
|
106
300
|
this.ttlMs = ttlMs;
|
|
107
301
|
this.maxEntries = maxEntries;
|
|
108
|
-
|
|
302
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs < 0)
|
|
303
|
+
throw new RangeError("ttlMs must be a non-negative safe integer.");
|
|
304
|
+
if (!Number.isInteger(maxEntries) || maxEntries < 1 || maxEntries > 1e4)
|
|
305
|
+
throw new RangeError("maxEntries must be between 1 and 10000.");
|
|
306
|
+
this.options = {
|
|
307
|
+
commandTimeoutMs: 5000,
|
|
308
|
+
leaseMs: 1e4,
|
|
309
|
+
fillTimeoutMs: 30000,
|
|
310
|
+
waitTimeoutMs: 35000,
|
|
311
|
+
pollMs: 25,
|
|
312
|
+
metadataTtlMs: 60000,
|
|
313
|
+
metadataLimit: 4096,
|
|
314
|
+
...options
|
|
315
|
+
};
|
|
316
|
+
for (const [key, value] of Object.entries(this.options))
|
|
317
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
318
|
+
throw new RangeError(`${key} must be a positive safe integer.`);
|
|
319
|
+
if (this.options.leaseMs < 30 || this.options.fillTimeoutMs < this.options.leaseMs || this.options.metadataTtlMs < this.options.fillTimeoutMs + this.options.leaseMs || this.options.metadataLimit < 66 || this.options.metadataLimit > 1e5)
|
|
320
|
+
throw new RangeError("Invalid Redis cache lease, deadline or metadata bounds.");
|
|
321
|
+
const namespace = namespacedRedisKey("cache:v3:");
|
|
322
|
+
const slot = new Bun.CryptoHasher("sha256").update(namespace).digest("hex");
|
|
323
|
+
this.keys = ["entries", "expiry", "order", "metadata", "metadata-expiry"].map((name) => `${namespace}{${slot}}:${name}`);
|
|
324
|
+
this.client = new RedisClient(redisUrl, {
|
|
325
|
+
connectionTimeout: this.options.commandTimeoutMs,
|
|
326
|
+
maxRetries: 1
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
close() {
|
|
330
|
+
this.closed = true;
|
|
331
|
+
for (const fill of this.fills)
|
|
332
|
+
fill.abort(new Error("Redis cache store is closed."));
|
|
333
|
+
this.client.close();
|
|
109
334
|
}
|
|
110
335
|
async get(key) {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
return JSON.parse(raw);
|
|
336
|
+
const result = await this.execute("get", key);
|
|
337
|
+
return result.kind === "value" ? JSON.parse(result.payload ?? "null") : undefined;
|
|
116
338
|
}
|
|
117
339
|
async set(key, value, ttlMs) {
|
|
118
|
-
|
|
119
|
-
const payload = JSON.stringify(value);
|
|
120
|
-
if (resolvedTtlMs > 0) {
|
|
121
|
-
await this.client.psetex(this.storageKey(key), resolvedTtlMs, payload);
|
|
122
|
-
} else {
|
|
123
|
-
await this.client.set(this.storageKey(key), payload);
|
|
124
|
-
}
|
|
125
|
-
await this.enforceMaxEntries();
|
|
340
|
+
await this.execute("set", key, { payload: this.serialize(value), ttlMs: ttlMs ?? this.ttlMs });
|
|
126
341
|
}
|
|
127
342
|
async getOrSet(key, loader, ttlMs) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
343
|
+
return this.getOrSetTagged(key, loader, [], ttlMs);
|
|
344
|
+
}
|
|
345
|
+
async getOrSetTagged(key, loader, tags, ttlMs) {
|
|
346
|
+
const owner = crypto.randomUUID();
|
|
347
|
+
const end = performance.now() + this.options.waitTimeoutMs;
|
|
348
|
+
while (!this.closed) {
|
|
349
|
+
const claim = await this.execute("claim", key, { owner, tags, ttlMs: ttlMs ?? this.ttlMs });
|
|
350
|
+
if (claim.kind === "value")
|
|
351
|
+
return JSON.parse(claim.payload ?? "null");
|
|
352
|
+
if (claim.kind === "owner")
|
|
353
|
+
return this.fill(key, owner, loader, ttlMs);
|
|
354
|
+
if (performance.now() >= end)
|
|
355
|
+
throw new Error("Redis cache fill wait deadline exceeded.");
|
|
356
|
+
await Bun.sleep(this.options.pollMs);
|
|
357
|
+
}
|
|
358
|
+
throw new Error("Redis cache store is closed.");
|
|
144
359
|
}
|
|
145
360
|
async attachTags(key, tags) {
|
|
146
|
-
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
let tagsForKey = this.keyTags.get(key);
|
|
150
|
-
if (!tagsForKey) {
|
|
151
|
-
tagsForKey = new Set;
|
|
152
|
-
this.keyTags.set(key, tagsForKey);
|
|
153
|
-
}
|
|
154
|
-
for (const tag of tags) {
|
|
155
|
-
tagsForKey.add(tag);
|
|
156
|
-
await this.client.sadd(this.tagKey(tag), key);
|
|
157
|
-
}
|
|
361
|
+
await this.execute("attach", key, { tags });
|
|
158
362
|
}
|
|
159
363
|
async flushTags(tags) {
|
|
160
|
-
|
|
161
|
-
for (const tag of tags) {
|
|
162
|
-
const members = await this.client.smembers(this.tagKey(tag));
|
|
163
|
-
for (const member of members) {
|
|
164
|
-
keysToRemove.add(member);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
let removed = 0;
|
|
168
|
-
for (const key of keysToRemove) {
|
|
169
|
-
if (await this.invalidate(key)) {
|
|
170
|
-
removed += 1;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
for (const tag of tags) {
|
|
174
|
-
await this.client.del(this.tagKey(tag));
|
|
175
|
-
}
|
|
176
|
-
return removed;
|
|
364
|
+
return (await this.execute("tags", "", { tags })).removed ?? 0;
|
|
177
365
|
}
|
|
178
366
|
async invalidate(key) {
|
|
179
|
-
|
|
180
|
-
await this.detachKeyFromTags(key);
|
|
181
|
-
return deleted > 0;
|
|
367
|
+
return ((await this.execute("invalidate", key)).removed ?? 0) > 0;
|
|
182
368
|
}
|
|
183
369
|
async invalidateByPrefix(prefix) {
|
|
184
|
-
|
|
185
|
-
let removed = 0;
|
|
186
|
-
for (const storageKey of keys) {
|
|
187
|
-
const key = storageKey.slice(cacheKeyPrefix().length);
|
|
188
|
-
if (key === prefix || key.startsWith(`${prefix}?`)) {
|
|
189
|
-
if (await this.invalidate(key)) {
|
|
190
|
-
removed += 1;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return removed;
|
|
370
|
+
return (await this.execute("prefix", prefix)).removed ?? 0;
|
|
195
371
|
}
|
|
196
372
|
async clear() {
|
|
197
|
-
|
|
198
|
-
if (keys.length > 0) {
|
|
199
|
-
await this.client.del(...keys);
|
|
200
|
-
}
|
|
201
|
-
const tagKeys = await this.client.keys(`${cacheTagPrefix()}*`);
|
|
202
|
-
if (tagKeys.length > 0) {
|
|
203
|
-
await this.client.del(...tagKeys);
|
|
204
|
-
}
|
|
205
|
-
this.inflight.clear();
|
|
206
|
-
this.keyTags.clear();
|
|
373
|
+
await this.execute("clear", "");
|
|
207
374
|
}
|
|
208
375
|
async size() {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
376
|
+
return (await this.execute("size", "")).size ?? 0;
|
|
377
|
+
}
|
|
378
|
+
async fill(key, owner, loader, ttlMs) {
|
|
379
|
+
if (this.fills.size >= this.maxEntries) {
|
|
380
|
+
await this.execute("release", key, { owner });
|
|
381
|
+
throw new Error("Redis cache local fill capacity exceeded.");
|
|
382
|
+
}
|
|
383
|
+
const controller = new AbortController;
|
|
384
|
+
this.fills.add(controller);
|
|
385
|
+
let done = false;
|
|
386
|
+
let renewing = false;
|
|
387
|
+
const renew = setInterval(() => {
|
|
388
|
+
if (renewing || done)
|
|
389
|
+
return;
|
|
390
|
+
renewing = true;
|
|
391
|
+
this.execute("renew", key, { owner }).then((result) => {
|
|
392
|
+
if (!result.owned)
|
|
393
|
+
clearInterval(renew);
|
|
394
|
+
}).catch((error) => {
|
|
395
|
+
if (!done)
|
|
396
|
+
controller.abort(error);
|
|
397
|
+
}).finally(() => {
|
|
398
|
+
renewing = false;
|
|
399
|
+
});
|
|
400
|
+
}, Math.max(10, Math.floor(this.options.leaseMs / 3)));
|
|
401
|
+
const timeout = setTimeout(() => controller.abort(new Error("Redis cache fill deadline exceeded.")), this.options.fillTimeoutMs);
|
|
402
|
+
let rejectAbort;
|
|
403
|
+
const aborted = new Promise((_, reject) => {
|
|
404
|
+
rejectAbort = () => reject(controller.signal.reason);
|
|
405
|
+
controller.signal.addEventListener("abort", rejectAbort, { once: true });
|
|
406
|
+
});
|
|
407
|
+
try {
|
|
408
|
+
const value = await Promise.race([
|
|
409
|
+
Promise.resolve().then(() => loader(controller.signal)),
|
|
410
|
+
aborted
|
|
411
|
+
]);
|
|
412
|
+
done = true;
|
|
413
|
+
clearInterval(renew);
|
|
414
|
+
await this.execute("publish", key, {
|
|
415
|
+
owner,
|
|
416
|
+
payload: this.serialize(value),
|
|
417
|
+
ttlMs: ttlMs ?? this.ttlMs
|
|
418
|
+
});
|
|
419
|
+
return value;
|
|
420
|
+
} finally {
|
|
421
|
+
done = true;
|
|
422
|
+
clearInterval(renew);
|
|
423
|
+
clearTimeout(timeout);
|
|
424
|
+
controller.signal.removeEventListener("abort", rejectAbort);
|
|
425
|
+
this.fills.delete(controller);
|
|
426
|
+
if (!this.closed)
|
|
427
|
+
await this.execute("release", key, { owner }).catch(() => {});
|
|
225
428
|
}
|
|
226
|
-
this.keyTags.delete(key);
|
|
227
429
|
}
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
430
|
+
serialize(value) {
|
|
431
|
+
const payload = JSON.stringify(value);
|
|
432
|
+
if (typeof payload !== "string")
|
|
433
|
+
throw new TypeError("Redis cache values must be JSON serializable.");
|
|
434
|
+
return payload;
|
|
435
|
+
}
|
|
436
|
+
async execute(op, key, extra = {}) {
|
|
437
|
+
if (this.closed)
|
|
438
|
+
throw new Error("Redis cache store is closed.");
|
|
439
|
+
if (Buffer.byteLength(key) > 4096)
|
|
440
|
+
throw new RangeError("Invalid Redis cache identity.");
|
|
441
|
+
const tags = Array.from(new Set(extra.tags ?? []));
|
|
442
|
+
if (tags.length > 32 || tags.some((tag) => typeof tag !== "string" || Buffer.byteLength(tag) > 1024))
|
|
443
|
+
throw new RangeError("Redis cache supports at most 32 tags of 1024 bytes each.");
|
|
444
|
+
const prefixes = [key];
|
|
445
|
+
for (let i = 0;i < key.length; i++)
|
|
446
|
+
if (key[i] === "?")
|
|
447
|
+
prefixes.push(key.slice(0, i));
|
|
448
|
+
if (prefixes.length > 33)
|
|
449
|
+
throw new RangeError("Redis cache identities support at most 32 query separators.");
|
|
450
|
+
const ttlMs = extra.ttlMs ?? this.ttlMs;
|
|
451
|
+
if (typeof ttlMs !== "number" || !Number.isSafeInteger(ttlMs) || ttlMs < 0)
|
|
452
|
+
throw new RangeError("ttlMs must be a non-negative safe integer.");
|
|
453
|
+
let timer;
|
|
454
|
+
const timeout = new Promise((_, reject) => {
|
|
455
|
+
timer = setTimeout(() => {
|
|
456
|
+
reject(new Error("Redis cache command deadline exceeded; recreate the store."));
|
|
457
|
+
this.close();
|
|
458
|
+
}, this.options.commandTimeoutMs);
|
|
459
|
+
});
|
|
460
|
+
try {
|
|
461
|
+
const raw = await Promise.race([
|
|
462
|
+
this.client.send("EVAL", [
|
|
463
|
+
CACHE_SCRIPT,
|
|
464
|
+
String(this.keys.length),
|
|
465
|
+
...this.keys,
|
|
466
|
+
JSON.stringify({
|
|
467
|
+
op,
|
|
468
|
+
key,
|
|
469
|
+
capacity: this.maxEntries,
|
|
470
|
+
owner: crypto.randomUUID(),
|
|
471
|
+
...this.options,
|
|
472
|
+
...extra,
|
|
473
|
+
tags,
|
|
474
|
+
prefixes,
|
|
475
|
+
ttlMs
|
|
476
|
+
})
|
|
477
|
+
]),
|
|
478
|
+
timeout
|
|
479
|
+
]);
|
|
480
|
+
return JSON.parse(String(raw));
|
|
481
|
+
} finally {
|
|
482
|
+
clearTimeout(timer);
|
|
237
483
|
}
|
|
238
484
|
}
|
|
239
485
|
}
|
|
@@ -459,7 +705,7 @@ function createCacheStore(options) {
|
|
|
459
705
|
if (!options.redisUrl) {
|
|
460
706
|
throw new Error('CACHE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
461
707
|
}
|
|
462
|
-
return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries);
|
|
708
|
+
return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries, options.redis);
|
|
463
709
|
}
|
|
464
710
|
return new simpleCacheStore_default(new simpleCache_default(options.ttlMs, options.maxEntries));
|
|
465
711
|
}
|
|
@@ -8,6 +8,8 @@ class TaggedCache {
|
|
|
8
8
|
this.tags = tags;
|
|
9
9
|
}
|
|
10
10
|
async remember(key, callback, ttlMs) {
|
|
11
|
+
if (this.store.getOrSetTagged)
|
|
12
|
+
return this.store.getOrSetTagged(key, callback, this.tags, ttlMs);
|
|
11
13
|
const value = await this.store.getOrSet(key, callback, ttlMs);
|
|
12
14
|
await this.store.attachTags(key, this.tags);
|
|
13
15
|
return value;
|
|
@@ -24,6 +26,9 @@ class CacheRepository {
|
|
|
24
26
|
constructor(store) {
|
|
25
27
|
this.store = store;
|
|
26
28
|
}
|
|
29
|
+
async close() {
|
|
30
|
+
await this.store.close?.();
|
|
31
|
+
}
|
|
27
32
|
async get(key) {
|
|
28
33
|
return this.store.get(key);
|
|
29
34
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/contracts/container.ts
|
|
3
|
+
function createServiceToken(key) {
|
|
4
|
+
if (!key.trim())
|
|
5
|
+
throw new Error("Service token key must not be empty.");
|
|
6
|
+
return key;
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
class ServiceContainer {
|
|
4
10
|
services = new Map;
|
|
5
11
|
singletonFactories = new Map;
|
|
@@ -71,5 +77,6 @@ class ConfigStore {
|
|
|
71
77
|
}
|
|
72
78
|
export {
|
|
73
79
|
ConfigStore,
|
|
74
|
-
ServiceContainer
|
|
80
|
+
ServiceContainer,
|
|
81
|
+
createServiceToken
|
|
75
82
|
};
|
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// ../../src/core/contracts/container.ts
|
|
3
|
+
function createServiceToken(key) {
|
|
4
|
+
if (!key.trim())
|
|
5
|
+
throw new Error("Service token key must not be empty.");
|
|
6
|
+
return key;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class ServiceContainer {
|
|
10
|
+
services = new Map;
|
|
11
|
+
singletonFactories = new Map;
|
|
12
|
+
bindings = new Map;
|
|
13
|
+
set(key, value) {
|
|
14
|
+
this.singletonFactories.delete(key);
|
|
15
|
+
this.bindings.delete(key);
|
|
16
|
+
this.services.set(key, value);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
singleton(key, factory) {
|
|
20
|
+
this.bindings.delete(key);
|
|
21
|
+
this.services.delete(key);
|
|
22
|
+
this.singletonFactories.set(key, factory);
|
|
23
|
+
}
|
|
24
|
+
bind(key, factory) {
|
|
25
|
+
this.singletonFactories.delete(key);
|
|
26
|
+
this.services.delete(key);
|
|
27
|
+
this.bindings.set(key, factory);
|
|
28
|
+
}
|
|
29
|
+
get(key) {
|
|
30
|
+
if (this.services.has(key)) {
|
|
31
|
+
return this.services.get(key);
|
|
32
|
+
}
|
|
33
|
+
const singletonFactory = this.singletonFactories.get(key);
|
|
34
|
+
if (singletonFactory) {
|
|
35
|
+
const value = singletonFactory(this);
|
|
36
|
+
this.services.set(key, value);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
const binding = this.bindings.get(key);
|
|
40
|
+
if (binding) {
|
|
41
|
+
return binding(this);
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Service "${key}" is not registered.`);
|
|
44
|
+
}
|
|
45
|
+
resolve(key) {
|
|
46
|
+
return this.get(key);
|
|
47
|
+
}
|
|
48
|
+
make(key) {
|
|
49
|
+
return this.resolve(key);
|
|
50
|
+
}
|
|
51
|
+
instance(key, value) {
|
|
52
|
+
return this.set(key, value);
|
|
53
|
+
}
|
|
54
|
+
has(key) {
|
|
55
|
+
return this.services.has(key) || this.singletonFactories.has(key) || this.bindings.has(key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class ConfigStore {
|
|
60
|
+
values = new Map;
|
|
61
|
+
set(key, value) {
|
|
62
|
+
this.values.set(key, value);
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
get(key) {
|
|
66
|
+
return this.values.get(key);
|
|
67
|
+
}
|
|
68
|
+
require(key) {
|
|
69
|
+
if (!this.values.has(key)) {
|
|
70
|
+
throw new Error(`Config key "${key}" is not defined.`);
|
|
71
|
+
}
|
|
72
|
+
return this.values.get(key);
|
|
73
|
+
}
|
|
74
|
+
has(key) {
|
|
75
|
+
return this.values.has(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
2
79
|
// ../../src/core/contracts/serviceTokens.ts
|
|
80
|
+
var CORE_THROTTLE_QUOTA_POLICY_TOKEN = createServiceToken("core.throttleQuotaPolicy");
|
|
81
|
+
var CORE_TENANT_RESOLVER_TOKEN = createServiceToken("core.tenantResolver");
|
|
82
|
+
var CORE_HTTP_CLEANUP_TOKEN = createServiceToken("core.httpCleanup");
|
|
3
83
|
var CORE_CONFIG_TOKEN = "core.config";
|
|
4
84
|
var CORE_CACHE_TOKEN = "core.cache";
|
|
5
85
|
var CORE_QUEUE_TOKEN = "core.queue";
|
|
@@ -50,8 +130,11 @@ export {
|
|
|
50
130
|
CORE_CACHE_TOKEN,
|
|
51
131
|
CORE_CONFIG_TOKEN,
|
|
52
132
|
CORE_EVENT_BUS_TOKEN,
|
|
133
|
+
CORE_HTTP_CLEANUP_TOKEN,
|
|
53
134
|
CORE_POLICY_GATE_TOKEN,
|
|
54
135
|
CORE_QUEUE_TOKEN,
|
|
136
|
+
CORE_TENANT_RESOLVER_TOKEN,
|
|
137
|
+
CORE_THROTTLE_QUOTA_POLICY_TOKEN,
|
|
55
138
|
CORE_TOKEN_SERVICE_TOKEN,
|
|
56
139
|
isAbilityChecker,
|
|
57
140
|
isAuthUserDirectory,
|
|
@@ -15,9 +15,6 @@ function singularize(word) {
|
|
|
15
15
|
function foreignKeyFromTable(tableName) {
|
|
16
16
|
return `${singularize(tableName)}_id`;
|
|
17
17
|
}
|
|
18
|
-
function pivotTableName(leftTable, rightTable) {
|
|
19
|
-
return [singularize(leftTable), singularize(rightTable)].sort().join("_");
|
|
20
|
-
}
|
|
21
18
|
|
|
22
19
|
// ../../src/core/database/factory.ts
|
|
23
20
|
function inferFactoryForeignKey(parent, explicit) {
|