@getstrata/core 2.1.0 → 2.2.1

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/core/cache/createCacheStore.d.ts +3 -1
  3. package/dist/core/cache/redisCacheStore.d.ts +21 -8
  4. package/dist/core/cache/repository.d.ts +3 -2
  5. package/dist/core/cache/store.d.ts +4 -1
  6. package/dist/core/cache/taggedCache.d.ts +1 -1
  7. package/dist/core/contracts/di.d.ts +4 -2
  8. package/dist/core/contracts/serviceTokens.d.ts +7 -1
  9. package/dist/core/database/baseRepository.d.ts +0 -1
  10. package/dist/core/database/index.d.ts +1 -1
  11. package/dist/core/database/model.d.ts +51 -22
  12. package/dist/core/database/repositoryQuery.d.ts +16 -1
  13. package/dist/core/database/types.d.ts +25 -2
  14. package/dist/core/database/whereBuilder.d.ts +5 -5
  15. package/dist/core/events/deferredModelEvents.d.ts +2 -1
  16. package/dist/core/http/loginThrottleMiddleware.d.ts +6 -5
  17. package/dist/core/http/memoryThrottleMiddleware.d.ts +11 -3
  18. package/dist/core/http/requestMetaContext.d.ts +2 -0
  19. package/dist/core/http/scimThrottleMiddleware.d.ts +12 -3
  20. package/dist/core/http/throttleMiddleware.d.ts +24 -6
  21. package/dist/core/lifecycle/gracefulShutdown.d.ts +33 -5
  22. package/dist/core/queue/createAppQueue.d.ts +2 -1
  23. package/dist/core/queue/failedJobService.d.ts +3 -1
  24. package/dist/core/queue/index.d.ts +17 -2
  25. package/dist/core/queue/jobRunner.d.ts +7 -1
  26. package/dist/core/queue/publicQueue.d.ts +2 -1
  27. package/dist/core/queue/queueConfig.d.ts +3 -0
  28. package/dist/core/queue/redisQueue.d.ts +6 -7
  29. package/dist/core/queue/redisQueueKeys.d.ts +8 -0
  30. package/dist/core/queue/redisStreams.d.ts +73 -0
  31. package/dist/core/queue/resilientQueue.d.ts +2 -2
  32. package/dist/core/queue/types.d.ts +1 -0
  33. package/dist/core/runtime/applicationRegistry.d.ts +2 -1
  34. package/dist/core/runtime/boundedThrottleStore.d.ts +29 -0
  35. package/dist/core/scheduler/leases.d.ts +40 -0
  36. package/dist/core/scheduler/schedule.d.ts +29 -5
  37. package/dist/core/tenant/publicTenantHost.d.ts +11 -0
  38. package/dist/core/tenant/tenantContext.d.ts +5 -4
  39. package/dist/core/tenant/tenantMiddleware.d.ts +11 -1
  40. package/dist/core/tracing/otel.d.ts +28 -26
  41. package/dist/core/tracing/tracingMiddleware.d.ts +4 -1
  42. package/dist/entries/audit/exportAuditLogs.js +14 -2
  43. package/dist/entries/auth/basicAuthGuard.js +81 -0
  44. package/dist/entries/auth/jwtGuard.js +81 -0
  45. package/dist/entries/auth/scimAuthMiddleware.js +17 -7
  46. package/dist/entries/auth/sessionGuard.js +81 -0
  47. package/dist/entries/cache/createCacheStore.js +365 -119
  48. package/dist/entries/cache/repository.js +5 -0
  49. package/dist/entries/contracts/serviceTokens.js +85 -0
  50. package/dist/entries/database/factory.js +0 -3
  51. package/dist/entries/database/model.js +1 -2486
  52. package/dist/entries/database/repositoryQuery.js +67 -9
  53. package/dist/entries/http/bodySizeLimitMiddleware.js +76 -10
  54. package/dist/entries/http/memoryThrottleMiddleware.js +276 -21
  55. package/dist/entries/http/scimThrottleMiddleware.js +576 -54
  56. package/dist/entries/http/throttleMiddleware.js +65 -16
  57. package/dist/entries/jobs/exportAuditLogsJob.js +44 -7
  58. package/dist/entries/jobs/invalidateCacheTagsJob.js +30 -5
  59. package/dist/entries/queue/createAppQueue.js +748 -72
  60. package/dist/entries/queue/failedJobRepository.js +1 -1
  61. package/dist/entries/queue/failedJobService.js +5 -1
  62. package/dist/entries/queue/jobRunner.js +67 -2
  63. package/dist/entries/queue/publicQueue.js +746 -71
  64. package/dist/entries/queue/queueMetrics.js +761 -78
  65. package/dist/entries/queue/redisQueue.js +768 -148
  66. package/dist/entries/queue.js +30 -5
  67. package/dist/entries/scheduler/schedule.js +1 -72
  68. package/dist/entries/tenant/databaseTenantContext.js +14 -2
  69. package/dist/entries/tenant/tenantDatabaseScope.js +14 -2
  70. package/dist/entries/tracing/tracingMiddleware.js +186 -454
  71. package/dist/framework/public-api.d.ts +14 -10
  72. package/dist/index.js +2444 -688
  73. package/dist/types/services.d.ts +3 -3
  74. package/package.json +17 -8
@@ -89,151 +89,397 @@ function sdkClientClassName() {
89
89
  }
90
90
 
91
91
  // ../../src/core/cache/redisCacheStore.ts
92
- function cacheKeyPrefix() {
93
- return namespacedRedisKey("cache:");
94
- }
95
- function cacheTagPrefix() {
96
- return namespacedRedisKey("cache:tag:");
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
- inflight = new Map;
104
- keyTags = new Map;
105
- constructor(redisUrl, ttlMs, maxEntries) {
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
- this.client = new RedisClient(redisUrl);
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 raw = await this.client.get(this.storageKey(key));
112
- if (raw === null) {
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
- const resolvedTtlMs = ttlMs ?? this.ttlMs;
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
- const cached = await this.get(key);
129
- if (cached !== undefined) {
130
- return cached;
131
- }
132
- const inflightRequest = this.inflight.get(key);
133
- if (inflightRequest) {
134
- return inflightRequest;
135
- }
136
- const pendingRequest = loader().then(async (value) => {
137
- await this.set(key, value, ttlMs);
138
- return value;
139
- }).finally(() => {
140
- this.inflight.delete(key);
141
- });
142
- this.inflight.set(key, pendingRequest);
143
- return pendingRequest;
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
- if (tags.length === 0) {
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
- const keysToRemove = new Set;
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
- const deleted = await this.client.del(this.storageKey(key));
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
- const keys = await this.client.keys(`${cacheKeyPrefix()}*`);
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
- const keys = await this.client.keys(`${cacheKeyPrefix()}*`);
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
- const keys = await this.client.keys(`${cacheKeyPrefix()}*`);
210
- return keys.length;
211
- }
212
- storageKey(key) {
213
- return `${cacheKeyPrefix()}${key}`;
214
- }
215
- tagKey(tag) {
216
- return `${cacheTagPrefix()}${tag}`;
217
- }
218
- async detachKeyFromTags(key) {
219
- const tags = this.keyTags.get(key);
220
- if (!tags) {
221
- return;
222
- }
223
- for (const tag of tags) {
224
- await this.client.srem(this.tagKey(tag), key);
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
- async enforceMaxEntries() {
229
- const keys = await this.client.keys(`${cacheKeyPrefix()}*`);
230
- if (keys.length <= this.maxEntries) {
231
- return;
232
- }
233
- const overflow = keys.length - this.maxEntries;
234
- const keysToRemove = keys.slice(0, overflow);
235
- if (keysToRemove.length > 0) {
236
- await this.client.del(...keysToRemove);
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,86 @@
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_PUBLIC_TENANCY_TOKEN = createServiceToken("core.publicTenancy");
82
+ var CORE_TENANT_RESOLVER_TOKEN = createServiceToken("core.tenantResolver");
83
+ var CORE_HTTP_CLEANUP_TOKEN = createServiceToken("core.httpCleanup");
3
84
  var CORE_CONFIG_TOKEN = "core.config";
4
85
  var CORE_CACHE_TOKEN = "core.cache";
5
86
  var CORE_QUEUE_TOKEN = "core.queue";
@@ -50,8 +131,12 @@ export {
50
131
  CORE_CACHE_TOKEN,
51
132
  CORE_CONFIG_TOKEN,
52
133
  CORE_EVENT_BUS_TOKEN,
134
+ CORE_HTTP_CLEANUP_TOKEN,
53
135
  CORE_POLICY_GATE_TOKEN,
136
+ CORE_PUBLIC_TENANCY_TOKEN,
54
137
  CORE_QUEUE_TOKEN,
138
+ CORE_TENANT_RESOLVER_TOKEN,
139
+ CORE_THROTTLE_QUOTA_POLICY_TOKEN,
55
140
  CORE_TOKEN_SERVICE_TOKEN,
56
141
  isAbilityChecker,
57
142
  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) {