@getstrata/core 0.5.15 → 0.5.16

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.
@@ -0,0 +1,11 @@
1
+ import type { Policy } from "../../core/auth/policy";
2
+ import type { RouteRequest } from "../../core/http/route";
3
+ interface RouteModelAuthorization {
4
+ resource: string;
5
+ action: keyof Policy;
6
+ requireIfMatch?: boolean;
7
+ }
8
+ declare function securedBindRouteModel<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (id: number, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
9
+ declare function securedBindRouteModelByKey<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (key: string, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
10
+ export type { RouteModelAuthorization };
11
+ export { securedBindRouteModel, securedBindRouteModelByKey };
@@ -1,11 +1,2 @@
1
- import type { Policy } from "../auth/policy";
2
- import type { RouteRequest } from "./route";
3
- interface RouteModelAuthorization {
4
- resource: string;
5
- action: keyof Policy;
6
- requireIfMatch?: boolean;
7
- }
8
- declare function securedBindRouteModel<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (id: number, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
9
- declare function securedBindRouteModelByKey<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (key: string, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
10
- export type { RouteModelAuthorization };
11
- export { securedBindRouteModel, securedBindRouteModelByKey };
1
+ export type { RouteModelAuthorization } from "../../bootstrap/http/securedRouteModelBinding.ts";
2
+ export { securedBindRouteModel, securedBindRouteModelByKey, } from "../../bootstrap/http/securedRouteModelBinding.ts";
@@ -0,0 +1,376 @@
1
+ // @bun
2
+ // ../../src/core/cache/redisCacheStore.ts
3
+ var {RedisClient } = globalThis.Bun;
4
+ var KEY_PREFIX = "workhub:cache:";
5
+ var TAG_PREFIX = "workhub:cache:tag:";
6
+
7
+ class RedisCacheStore {
8
+ ttlMs;
9
+ maxEntries;
10
+ client;
11
+ inflight = new Map;
12
+ keyTags = new Map;
13
+ constructor(redisUrl, ttlMs, maxEntries) {
14
+ this.ttlMs = ttlMs;
15
+ this.maxEntries = maxEntries;
16
+ this.client = new RedisClient(redisUrl);
17
+ }
18
+ async get(key) {
19
+ const raw = await this.client.get(this.storageKey(key));
20
+ if (raw === null) {
21
+ return;
22
+ }
23
+ return JSON.parse(raw);
24
+ }
25
+ async set(key, value, ttlMs) {
26
+ const resolvedTtlMs = ttlMs ?? this.ttlMs;
27
+ const payload = JSON.stringify(value);
28
+ if (resolvedTtlMs > 0) {
29
+ await this.client.psetex(this.storageKey(key), resolvedTtlMs, payload);
30
+ } else {
31
+ await this.client.set(this.storageKey(key), payload);
32
+ }
33
+ await this.enforceMaxEntries();
34
+ }
35
+ async getOrSet(key, loader, ttlMs) {
36
+ const cached = await this.get(key);
37
+ if (cached !== undefined) {
38
+ return cached;
39
+ }
40
+ const inflightRequest = this.inflight.get(key);
41
+ if (inflightRequest) {
42
+ return inflightRequest;
43
+ }
44
+ const pendingRequest = loader().then(async (value) => {
45
+ await this.set(key, value, ttlMs);
46
+ return value;
47
+ }).finally(() => {
48
+ this.inflight.delete(key);
49
+ });
50
+ this.inflight.set(key, pendingRequest);
51
+ return pendingRequest;
52
+ }
53
+ async attachTags(key, tags) {
54
+ if (tags.length === 0) {
55
+ return;
56
+ }
57
+ let tagsForKey = this.keyTags.get(key);
58
+ if (!tagsForKey) {
59
+ tagsForKey = new Set;
60
+ this.keyTags.set(key, tagsForKey);
61
+ }
62
+ for (const tag of tags) {
63
+ tagsForKey.add(tag);
64
+ await this.client.sadd(this.tagKey(tag), key);
65
+ }
66
+ }
67
+ async flushTags(tags) {
68
+ const keysToRemove = new Set;
69
+ for (const tag of tags) {
70
+ const members = await this.client.smembers(this.tagKey(tag));
71
+ for (const member of members) {
72
+ keysToRemove.add(member);
73
+ }
74
+ }
75
+ let removed = 0;
76
+ for (const key of keysToRemove) {
77
+ if (await this.invalidate(key)) {
78
+ removed += 1;
79
+ }
80
+ }
81
+ for (const tag of tags) {
82
+ await this.client.del(this.tagKey(tag));
83
+ }
84
+ return removed;
85
+ }
86
+ async invalidate(key) {
87
+ const deleted = await this.client.del(this.storageKey(key));
88
+ await this.detachKeyFromTags(key);
89
+ return deleted > 0;
90
+ }
91
+ async invalidateByPrefix(prefix) {
92
+ const keys = await this.client.keys(`${KEY_PREFIX}*`);
93
+ let removed = 0;
94
+ for (const storageKey of keys) {
95
+ const key = storageKey.slice(KEY_PREFIX.length);
96
+ if (key === prefix || key.startsWith(`${prefix}?`)) {
97
+ if (await this.invalidate(key)) {
98
+ removed += 1;
99
+ }
100
+ }
101
+ }
102
+ return removed;
103
+ }
104
+ async clear() {
105
+ const keys = await this.client.keys(`${KEY_PREFIX}*`);
106
+ if (keys.length > 0) {
107
+ await this.client.del(...keys);
108
+ }
109
+ const tagKeys = await this.client.keys(`${TAG_PREFIX}*`);
110
+ if (tagKeys.length > 0) {
111
+ await this.client.del(...tagKeys);
112
+ }
113
+ this.inflight.clear();
114
+ this.keyTags.clear();
115
+ }
116
+ async size() {
117
+ const keys = await this.client.keys(`${KEY_PREFIX}*`);
118
+ return keys.length;
119
+ }
120
+ storageKey(key) {
121
+ return `${KEY_PREFIX}${key}`;
122
+ }
123
+ tagKey(tag) {
124
+ return `${TAG_PREFIX}${tag}`;
125
+ }
126
+ async detachKeyFromTags(key) {
127
+ const tags = this.keyTags.get(key);
128
+ if (!tags) {
129
+ return;
130
+ }
131
+ for (const tag of tags) {
132
+ await this.client.srem(this.tagKey(tag), key);
133
+ }
134
+ this.keyTags.delete(key);
135
+ }
136
+ async enforceMaxEntries() {
137
+ const keys = await this.client.keys(`${KEY_PREFIX}*`);
138
+ if (keys.length <= this.maxEntries) {
139
+ return;
140
+ }
141
+ const overflow = keys.length - this.maxEntries;
142
+ const keysToRemove = keys.slice(0, overflow);
143
+ if (keysToRemove.length > 0) {
144
+ await this.client.del(...keysToRemove);
145
+ }
146
+ }
147
+ }
148
+ var redisCacheStore_default = RedisCacheStore;
149
+
150
+ // ../../src/core/cache/simpleCache.ts
151
+ class SimpleCache {
152
+ ttlMs;
153
+ maxEntries;
154
+ cache = new Map;
155
+ inflight = new Map;
156
+ tagIndex = new Map;
157
+ keyTags = new Map;
158
+ constructor(ttlMs = 3600000, maxEntries = 100) {
159
+ this.ttlMs = ttlMs;
160
+ this.maxEntries = maxEntries;
161
+ if (!Number.isFinite(ttlMs) || ttlMs < 0) {
162
+ throw new RangeError("ttlMs must be a non-negative number.");
163
+ }
164
+ if (!Number.isInteger(maxEntries) || maxEntries < 1) {
165
+ throw new RangeError("maxEntries must be a positive integer.");
166
+ }
167
+ }
168
+ get(key) {
169
+ return this.getFreshEntry(key)?.value;
170
+ }
171
+ set(key, value, ttlMs) {
172
+ const now = Date.now();
173
+ const resolvedTtlMs = ttlMs ?? this.ttlMs;
174
+ this.cache.set(key, {
175
+ value,
176
+ expiresAt: now + resolvedTtlMs,
177
+ lastAccessedAt: now
178
+ });
179
+ this.evictOverflow();
180
+ }
181
+ async getOrSet(key, loader, ttlMs) {
182
+ this.pruneExpired();
183
+ const cachedEntry = this.getFreshEntry(key);
184
+ if (cachedEntry) {
185
+ return cachedEntry.value;
186
+ }
187
+ const inflightRequest = this.inflight.get(key);
188
+ if (inflightRequest) {
189
+ return inflightRequest;
190
+ }
191
+ const pendingRequest = loader().then((value) => {
192
+ this.set(key, value, ttlMs);
193
+ return value;
194
+ }).finally(() => {
195
+ this.inflight.delete(key);
196
+ });
197
+ this.inflight.set(key, pendingRequest);
198
+ return pendingRequest;
199
+ }
200
+ attachTags(key, tags) {
201
+ if (tags.length === 0) {
202
+ return;
203
+ }
204
+ let tagsForKey = this.keyTags.get(key);
205
+ if (!tagsForKey) {
206
+ tagsForKey = new Set;
207
+ this.keyTags.set(key, tagsForKey);
208
+ }
209
+ for (const tag of tags) {
210
+ tagsForKey.add(tag);
211
+ let keysForTag = this.tagIndex.get(tag);
212
+ if (!keysForTag) {
213
+ keysForTag = new Set;
214
+ this.tagIndex.set(tag, keysForTag);
215
+ }
216
+ keysForTag.add(key);
217
+ }
218
+ }
219
+ flushTags(tags) {
220
+ const keysToRemove = new Set;
221
+ for (const tag of tags) {
222
+ const keys = this.tagIndex.get(tag);
223
+ if (!keys) {
224
+ continue;
225
+ }
226
+ for (const key of keys) {
227
+ keysToRemove.add(key);
228
+ }
229
+ }
230
+ let removed = 0;
231
+ for (const key of keysToRemove) {
232
+ if (this.invalidate(key)) {
233
+ removed += 1;
234
+ }
235
+ }
236
+ for (const tag of tags) {
237
+ this.tagIndex.delete(tag);
238
+ }
239
+ return removed;
240
+ }
241
+ invalidate(key) {
242
+ const removed = this.cache.delete(key);
243
+ if (removed) {
244
+ this.detachKeyFromTags(key);
245
+ }
246
+ return removed;
247
+ }
248
+ invalidateByPrefix(prefix) {
249
+ let removed = 0;
250
+ for (const key of [...this.cache.keys()]) {
251
+ if (key === prefix || key.startsWith(`${prefix}?`)) {
252
+ if (this.invalidate(key)) {
253
+ removed += 1;
254
+ }
255
+ }
256
+ }
257
+ return removed;
258
+ }
259
+ clear() {
260
+ this.cache.clear();
261
+ this.inflight.clear();
262
+ this.tagIndex.clear();
263
+ this.keyTags.clear();
264
+ }
265
+ size() {
266
+ this.pruneExpired();
267
+ return this.cache.size;
268
+ }
269
+ detachKeyFromTags(key) {
270
+ const tags = this.keyTags.get(key);
271
+ if (!tags) {
272
+ return;
273
+ }
274
+ for (const tag of tags) {
275
+ const keys = this.tagIndex.get(tag);
276
+ if (!keys) {
277
+ continue;
278
+ }
279
+ keys.delete(key);
280
+ if (keys.size === 0) {
281
+ this.tagIndex.delete(tag);
282
+ }
283
+ }
284
+ this.keyTags.delete(key);
285
+ }
286
+ getFreshEntry(key) {
287
+ const entry = this.cache.get(key);
288
+ if (!entry) {
289
+ return;
290
+ }
291
+ if (entry.expiresAt <= Date.now()) {
292
+ this.invalidate(key);
293
+ return;
294
+ }
295
+ entry.lastAccessedAt = Date.now();
296
+ return entry;
297
+ }
298
+ pruneExpired() {
299
+ const now = Date.now();
300
+ for (const [key, entry] of this.cache.entries()) {
301
+ if (entry.expiresAt <= now) {
302
+ this.invalidate(key);
303
+ }
304
+ }
305
+ }
306
+ evictOverflow() {
307
+ while (this.cache.size > this.maxEntries) {
308
+ let oldestKey;
309
+ let oldestAccessTime = Number.POSITIVE_INFINITY;
310
+ for (const [key, entry] of this.cache.entries()) {
311
+ if (entry.lastAccessedAt < oldestAccessTime) {
312
+ oldestAccessTime = entry.lastAccessedAt;
313
+ oldestKey = key;
314
+ }
315
+ }
316
+ if (!oldestKey) {
317
+ return;
318
+ }
319
+ this.invalidate(oldestKey);
320
+ }
321
+ }
322
+ }
323
+ var simpleCache_default = SimpleCache;
324
+
325
+ // ../../src/core/cache/simpleCacheStore.ts
326
+ class SimpleCacheStore {
327
+ cache;
328
+ constructor(cache) {
329
+ this.cache = cache;
330
+ }
331
+ get(key) {
332
+ return Promise.resolve(this.cache.get(key));
333
+ }
334
+ set(key, value, ttlMs) {
335
+ this.cache.set(key, value, ttlMs);
336
+ return Promise.resolve();
337
+ }
338
+ getOrSet(key, loader, ttlMs) {
339
+ return this.cache.getOrSet(key, loader, ttlMs);
340
+ }
341
+ attachTags(key, tags) {
342
+ this.cache.attachTags(key, tags);
343
+ return Promise.resolve();
344
+ }
345
+ flushTags(tags) {
346
+ return Promise.resolve(this.cache.flushTags(tags));
347
+ }
348
+ invalidate(key) {
349
+ return Promise.resolve(this.cache.invalidate(key));
350
+ }
351
+ invalidateByPrefix(prefix) {
352
+ return Promise.resolve(this.cache.invalidateByPrefix(prefix));
353
+ }
354
+ clear() {
355
+ this.cache.clear();
356
+ return Promise.resolve();
357
+ }
358
+ size() {
359
+ return Promise.resolve(this.cache.size());
360
+ }
361
+ }
362
+ var simpleCacheStore_default = SimpleCacheStore;
363
+
364
+ // ../../src/core/cache/createCacheStore.ts
365
+ function createCacheStore(options) {
366
+ if (options.driver === "redis") {
367
+ if (!options.redisUrl) {
368
+ throw new Error('CACHE_DRIVER="redis" requires REDIS_URL to be set.');
369
+ }
370
+ return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries);
371
+ }
372
+ return new simpleCacheStore_default(new simpleCache_default(options.ttlMs, options.maxEntries));
373
+ }
374
+ export {
375
+ createCacheStore
376
+ };
@@ -19,6 +19,7 @@ export { appendOrganizationScope, appendProjectScope, assertOrganizationReadable
19
19
  export { default as MembershipService, resolveMembershipService, } from "../core/auth/membershipService.ts";
20
20
  export { Policy, PolicyGate } from "../core/auth/policy.ts";
21
21
  export { createScimAuthMiddleware } from "../core/auth/scimAuthMiddleware.ts";
22
+ export { type CacheDriver, type CreateCacheStoreOptions, createCacheStore, } from "../core/cache/createCacheStore.ts";
22
23
  export { default as CacheRepository } from "../core/cache/repository.ts";
23
24
  export { CACHE_TAGS } from "../core/cache/tags.ts";
24
25
  export type { DatabaseConnection } from "../core/database/baseRepository.ts";
package/dist/index.js CHANGED
@@ -3526,6 +3526,378 @@ function jsonScimError(detail, status) {
3526
3526
  headers: { "content-type": "application/scim+json" }
3527
3527
  });
3528
3528
  }
3529
+ // ../../src/core/cache/redisCacheStore.ts
3530
+ var {RedisClient } = globalThis.Bun;
3531
+ var KEY_PREFIX = "workhub:cache:";
3532
+ var TAG_PREFIX = "workhub:cache:tag:";
3533
+
3534
+ class RedisCacheStore {
3535
+ ttlMs;
3536
+ maxEntries;
3537
+ client;
3538
+ inflight = new Map;
3539
+ keyTags = new Map;
3540
+ constructor(redisUrl, ttlMs, maxEntries) {
3541
+ this.ttlMs = ttlMs;
3542
+ this.maxEntries = maxEntries;
3543
+ this.client = new RedisClient(redisUrl);
3544
+ }
3545
+ async get(key) {
3546
+ const raw = await this.client.get(this.storageKey(key));
3547
+ if (raw === null) {
3548
+ return;
3549
+ }
3550
+ return JSON.parse(raw);
3551
+ }
3552
+ async set(key, value, ttlMs) {
3553
+ const resolvedTtlMs = ttlMs ?? this.ttlMs;
3554
+ const payload = JSON.stringify(value);
3555
+ if (resolvedTtlMs > 0) {
3556
+ await this.client.psetex(this.storageKey(key), resolvedTtlMs, payload);
3557
+ } else {
3558
+ await this.client.set(this.storageKey(key), payload);
3559
+ }
3560
+ await this.enforceMaxEntries();
3561
+ }
3562
+ async getOrSet(key, loader, ttlMs) {
3563
+ const cached = await this.get(key);
3564
+ if (cached !== undefined) {
3565
+ return cached;
3566
+ }
3567
+ const inflightRequest = this.inflight.get(key);
3568
+ if (inflightRequest) {
3569
+ return inflightRequest;
3570
+ }
3571
+ const pendingRequest = loader().then(async (value) => {
3572
+ await this.set(key, value, ttlMs);
3573
+ return value;
3574
+ }).finally(() => {
3575
+ this.inflight.delete(key);
3576
+ });
3577
+ this.inflight.set(key, pendingRequest);
3578
+ return pendingRequest;
3579
+ }
3580
+ async attachTags(key, tags) {
3581
+ if (tags.length === 0) {
3582
+ return;
3583
+ }
3584
+ let tagsForKey = this.keyTags.get(key);
3585
+ if (!tagsForKey) {
3586
+ tagsForKey = new Set;
3587
+ this.keyTags.set(key, tagsForKey);
3588
+ }
3589
+ for (const tag of tags) {
3590
+ tagsForKey.add(tag);
3591
+ await this.client.sadd(this.tagKey(tag), key);
3592
+ }
3593
+ }
3594
+ async flushTags(tags) {
3595
+ const keysToRemove = new Set;
3596
+ for (const tag of tags) {
3597
+ const members = await this.client.smembers(this.tagKey(tag));
3598
+ for (const member of members) {
3599
+ keysToRemove.add(member);
3600
+ }
3601
+ }
3602
+ let removed = 0;
3603
+ for (const key of keysToRemove) {
3604
+ if (await this.invalidate(key)) {
3605
+ removed += 1;
3606
+ }
3607
+ }
3608
+ for (const tag of tags) {
3609
+ await this.client.del(this.tagKey(tag));
3610
+ }
3611
+ return removed;
3612
+ }
3613
+ async invalidate(key) {
3614
+ const deleted = await this.client.del(this.storageKey(key));
3615
+ await this.detachKeyFromTags(key);
3616
+ return deleted > 0;
3617
+ }
3618
+ async invalidateByPrefix(prefix) {
3619
+ const keys = await this.client.keys(`${KEY_PREFIX}*`);
3620
+ let removed = 0;
3621
+ for (const storageKey of keys) {
3622
+ const key = storageKey.slice(KEY_PREFIX.length);
3623
+ if (key === prefix || key.startsWith(`${prefix}?`)) {
3624
+ if (await this.invalidate(key)) {
3625
+ removed += 1;
3626
+ }
3627
+ }
3628
+ }
3629
+ return removed;
3630
+ }
3631
+ async clear() {
3632
+ const keys = await this.client.keys(`${KEY_PREFIX}*`);
3633
+ if (keys.length > 0) {
3634
+ await this.client.del(...keys);
3635
+ }
3636
+ const tagKeys = await this.client.keys(`${TAG_PREFIX}*`);
3637
+ if (tagKeys.length > 0) {
3638
+ await this.client.del(...tagKeys);
3639
+ }
3640
+ this.inflight.clear();
3641
+ this.keyTags.clear();
3642
+ }
3643
+ async size() {
3644
+ const keys = await this.client.keys(`${KEY_PREFIX}*`);
3645
+ return keys.length;
3646
+ }
3647
+ storageKey(key) {
3648
+ return `${KEY_PREFIX}${key}`;
3649
+ }
3650
+ tagKey(tag) {
3651
+ return `${TAG_PREFIX}${tag}`;
3652
+ }
3653
+ async detachKeyFromTags(key) {
3654
+ const tags = this.keyTags.get(key);
3655
+ if (!tags) {
3656
+ return;
3657
+ }
3658
+ for (const tag of tags) {
3659
+ await this.client.srem(this.tagKey(tag), key);
3660
+ }
3661
+ this.keyTags.delete(key);
3662
+ }
3663
+ async enforceMaxEntries() {
3664
+ const keys = await this.client.keys(`${KEY_PREFIX}*`);
3665
+ if (keys.length <= this.maxEntries) {
3666
+ return;
3667
+ }
3668
+ const overflow = keys.length - this.maxEntries;
3669
+ const keysToRemove = keys.slice(0, overflow);
3670
+ if (keysToRemove.length > 0) {
3671
+ await this.client.del(...keysToRemove);
3672
+ }
3673
+ }
3674
+ }
3675
+ var redisCacheStore_default = RedisCacheStore;
3676
+
3677
+ // ../../src/core/cache/simpleCache.ts
3678
+ class SimpleCache {
3679
+ ttlMs;
3680
+ maxEntries;
3681
+ cache = new Map;
3682
+ inflight = new Map;
3683
+ tagIndex = new Map;
3684
+ keyTags = new Map;
3685
+ constructor(ttlMs = 3600000, maxEntries = 100) {
3686
+ this.ttlMs = ttlMs;
3687
+ this.maxEntries = maxEntries;
3688
+ if (!Number.isFinite(ttlMs) || ttlMs < 0) {
3689
+ throw new RangeError("ttlMs must be a non-negative number.");
3690
+ }
3691
+ if (!Number.isInteger(maxEntries) || maxEntries < 1) {
3692
+ throw new RangeError("maxEntries must be a positive integer.");
3693
+ }
3694
+ }
3695
+ get(key) {
3696
+ return this.getFreshEntry(key)?.value;
3697
+ }
3698
+ set(key, value, ttlMs) {
3699
+ const now = Date.now();
3700
+ const resolvedTtlMs = ttlMs ?? this.ttlMs;
3701
+ this.cache.set(key, {
3702
+ value,
3703
+ expiresAt: now + resolvedTtlMs,
3704
+ lastAccessedAt: now
3705
+ });
3706
+ this.evictOverflow();
3707
+ }
3708
+ async getOrSet(key, loader, ttlMs) {
3709
+ this.pruneExpired();
3710
+ const cachedEntry = this.getFreshEntry(key);
3711
+ if (cachedEntry) {
3712
+ return cachedEntry.value;
3713
+ }
3714
+ const inflightRequest = this.inflight.get(key);
3715
+ if (inflightRequest) {
3716
+ return inflightRequest;
3717
+ }
3718
+ const pendingRequest = loader().then((value) => {
3719
+ this.set(key, value, ttlMs);
3720
+ return value;
3721
+ }).finally(() => {
3722
+ this.inflight.delete(key);
3723
+ });
3724
+ this.inflight.set(key, pendingRequest);
3725
+ return pendingRequest;
3726
+ }
3727
+ attachTags(key, tags) {
3728
+ if (tags.length === 0) {
3729
+ return;
3730
+ }
3731
+ let tagsForKey = this.keyTags.get(key);
3732
+ if (!tagsForKey) {
3733
+ tagsForKey = new Set;
3734
+ this.keyTags.set(key, tagsForKey);
3735
+ }
3736
+ for (const tag of tags) {
3737
+ tagsForKey.add(tag);
3738
+ let keysForTag = this.tagIndex.get(tag);
3739
+ if (!keysForTag) {
3740
+ keysForTag = new Set;
3741
+ this.tagIndex.set(tag, keysForTag);
3742
+ }
3743
+ keysForTag.add(key);
3744
+ }
3745
+ }
3746
+ flushTags(tags) {
3747
+ const keysToRemove = new Set;
3748
+ for (const tag of tags) {
3749
+ const keys = this.tagIndex.get(tag);
3750
+ if (!keys) {
3751
+ continue;
3752
+ }
3753
+ for (const key of keys) {
3754
+ keysToRemove.add(key);
3755
+ }
3756
+ }
3757
+ let removed = 0;
3758
+ for (const key of keysToRemove) {
3759
+ if (this.invalidate(key)) {
3760
+ removed += 1;
3761
+ }
3762
+ }
3763
+ for (const tag of tags) {
3764
+ this.tagIndex.delete(tag);
3765
+ }
3766
+ return removed;
3767
+ }
3768
+ invalidate(key) {
3769
+ const removed = this.cache.delete(key);
3770
+ if (removed) {
3771
+ this.detachKeyFromTags(key);
3772
+ }
3773
+ return removed;
3774
+ }
3775
+ invalidateByPrefix(prefix) {
3776
+ let removed = 0;
3777
+ for (const key of [...this.cache.keys()]) {
3778
+ if (key === prefix || key.startsWith(`${prefix}?`)) {
3779
+ if (this.invalidate(key)) {
3780
+ removed += 1;
3781
+ }
3782
+ }
3783
+ }
3784
+ return removed;
3785
+ }
3786
+ clear() {
3787
+ this.cache.clear();
3788
+ this.inflight.clear();
3789
+ this.tagIndex.clear();
3790
+ this.keyTags.clear();
3791
+ }
3792
+ size() {
3793
+ this.pruneExpired();
3794
+ return this.cache.size;
3795
+ }
3796
+ detachKeyFromTags(key) {
3797
+ const tags = this.keyTags.get(key);
3798
+ if (!tags) {
3799
+ return;
3800
+ }
3801
+ for (const tag of tags) {
3802
+ const keys = this.tagIndex.get(tag);
3803
+ if (!keys) {
3804
+ continue;
3805
+ }
3806
+ keys.delete(key);
3807
+ if (keys.size === 0) {
3808
+ this.tagIndex.delete(tag);
3809
+ }
3810
+ }
3811
+ this.keyTags.delete(key);
3812
+ }
3813
+ getFreshEntry(key) {
3814
+ const entry = this.cache.get(key);
3815
+ if (!entry) {
3816
+ return;
3817
+ }
3818
+ if (entry.expiresAt <= Date.now()) {
3819
+ this.invalidate(key);
3820
+ return;
3821
+ }
3822
+ entry.lastAccessedAt = Date.now();
3823
+ return entry;
3824
+ }
3825
+ pruneExpired() {
3826
+ const now = Date.now();
3827
+ for (const [key, entry] of this.cache.entries()) {
3828
+ if (entry.expiresAt <= now) {
3829
+ this.invalidate(key);
3830
+ }
3831
+ }
3832
+ }
3833
+ evictOverflow() {
3834
+ while (this.cache.size > this.maxEntries) {
3835
+ let oldestKey;
3836
+ let oldestAccessTime = Number.POSITIVE_INFINITY;
3837
+ for (const [key, entry] of this.cache.entries()) {
3838
+ if (entry.lastAccessedAt < oldestAccessTime) {
3839
+ oldestAccessTime = entry.lastAccessedAt;
3840
+ oldestKey = key;
3841
+ }
3842
+ }
3843
+ if (!oldestKey) {
3844
+ return;
3845
+ }
3846
+ this.invalidate(oldestKey);
3847
+ }
3848
+ }
3849
+ }
3850
+ var simpleCache_default = SimpleCache;
3851
+
3852
+ // ../../src/core/cache/simpleCacheStore.ts
3853
+ class SimpleCacheStore {
3854
+ cache;
3855
+ constructor(cache) {
3856
+ this.cache = cache;
3857
+ }
3858
+ get(key) {
3859
+ return Promise.resolve(this.cache.get(key));
3860
+ }
3861
+ set(key, value, ttlMs) {
3862
+ this.cache.set(key, value, ttlMs);
3863
+ return Promise.resolve();
3864
+ }
3865
+ getOrSet(key, loader, ttlMs) {
3866
+ return this.cache.getOrSet(key, loader, ttlMs);
3867
+ }
3868
+ attachTags(key, tags) {
3869
+ this.cache.attachTags(key, tags);
3870
+ return Promise.resolve();
3871
+ }
3872
+ flushTags(tags) {
3873
+ return Promise.resolve(this.cache.flushTags(tags));
3874
+ }
3875
+ invalidate(key) {
3876
+ return Promise.resolve(this.cache.invalidate(key));
3877
+ }
3878
+ invalidateByPrefix(prefix) {
3879
+ return Promise.resolve(this.cache.invalidateByPrefix(prefix));
3880
+ }
3881
+ clear() {
3882
+ this.cache.clear();
3883
+ return Promise.resolve();
3884
+ }
3885
+ size() {
3886
+ return Promise.resolve(this.cache.size());
3887
+ }
3888
+ }
3889
+ var simpleCacheStore_default = SimpleCacheStore;
3890
+
3891
+ // ../../src/core/cache/createCacheStore.ts
3892
+ function createCacheStore(options) {
3893
+ if (options.driver === "redis") {
3894
+ if (!options.redisUrl) {
3895
+ throw new Error('CACHE_DRIVER="redis" requires REDIS_URL to be set.');
3896
+ }
3897
+ return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries);
3898
+ }
3899
+ return new simpleCacheStore_default(new simpleCache_default(options.ttlMs, options.maxEntries));
3900
+ }
3529
3901
  // ../../src/core/cache/taggedCache.ts
3530
3902
  class TaggedCache {
3531
3903
  store;
@@ -4894,7 +5266,7 @@ function bindRouteModel(param, resolver, handler) {
4894
5266
  return await handler(request, model);
4895
5267
  };
4896
5268
  }
4897
- // ../../src/core/http/securedRouteModelBinding.ts
5269
+ // ../../src/bootstrap/http/securedRouteModelBinding.ts
4898
5270
  function isMutatingPolicyAction(action) {
4899
5271
  return action === "update" || action === "delete";
4900
5272
  }
@@ -5050,7 +5422,7 @@ function withErrorHandling(handler) {
5050
5422
  };
5051
5423
  }
5052
5424
  // ../../src/core/http/loginThrottleMiddleware.ts
5053
- var {RedisClient } = globalThis.Bun;
5425
+ var {RedisClient: RedisClient2 } = globalThis.Bun;
5054
5426
  function resolveLoginIdentity(request) {
5055
5427
  return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
5056
5428
  }
@@ -5069,7 +5441,7 @@ async function resolveLoginEmail(request) {
5069
5441
  }
5070
5442
  }
5071
5443
  function createLoginThrottleMiddleware(options) {
5072
- const client = new RedisClient(options.redisUrl);
5444
+ const client = new RedisClient2(options.redisUrl);
5073
5445
  const prefix = options.keyPrefix ?? "workhub:login-throttle:";
5074
5446
  return async (request, next) => {
5075
5447
  const identity = resolveLoginIdentity(request);
@@ -5249,9 +5621,9 @@ function createRequireWebAuthMiddleware(auth2) {
5249
5621
  };
5250
5622
  }
5251
5623
  // ../../src/core/http/scimThrottleMiddleware.ts
5252
- var {RedisClient: RedisClient2 } = globalThis.Bun;
5624
+ var {RedisClient: RedisClient3 } = globalThis.Bun;
5253
5625
  function createScimThrottleMiddleware(options) {
5254
- const client = options.redisUrl ? new RedisClient2(options.redisUrl) : null;
5626
+ const client = options.redisUrl ? new RedisClient3(options.redisUrl) : null;
5255
5627
  return async (request, next) => {
5256
5628
  const identity = request.headers.get("authorization")?.slice("Bearer ".length, "Bearer ".length + 16) ?? request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
5257
5629
  const key = `workhub:scim-throttle:${identity}`;
@@ -5341,7 +5713,7 @@ function createSecurityHeadersMiddleware() {
5341
5713
  };
5342
5714
  }
5343
5715
  // ../../src/core/http/throttleMiddleware.ts
5344
- var {RedisClient: RedisClient3 } = globalThis.Bun;
5716
+ var {RedisClient: RedisClient4 } = globalThis.Bun;
5345
5717
  function resolveThrottleIdentity(request) {
5346
5718
  const user = currentAuthUser();
5347
5719
  if (user?.tokenId !== undefined) {
@@ -5353,7 +5725,7 @@ function resolveThrottleIdentity(request) {
5353
5725
  return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
5354
5726
  }
5355
5727
  function createThrottleMiddleware(options) {
5356
- const client = new RedisClient3(options.redisUrl);
5728
+ const client = new RedisClient4(options.redisUrl);
5357
5729
  const prefix = options.keyPrefix ?? "workhub:throttle:";
5358
5730
  return async (request, next) => {
5359
5731
  const identity = resolveThrottleIdentity(request);
@@ -5772,7 +6144,7 @@ async function runQueueJob(envelope, failedJobs) {
5772
6144
  }
5773
6145
 
5774
6146
  // ../../src/core/queue/redisQueue.ts
5775
- var {RedisClient: RedisClient4 } = globalThis.Bun;
6147
+ var {RedisClient: RedisClient5 } = globalThis.Bun;
5776
6148
  var QUEUE_LIST_KEY = "workhub:queue:default";
5777
6149
  var QUEUE_HIGH_KEY = "workhub:queue:high";
5778
6150
  var QUEUE_LOW_KEY = "workhub:queue:low";
@@ -5822,7 +6194,7 @@ function parseQueueJobEnvelope(rawPayload) {
5822
6194
  class RedisQueue {
5823
6195
  client;
5824
6196
  constructor(redisUrl) {
5825
- this.client = new RedisClient4(redisUrl);
6197
+ this.client = new RedisClient5(redisUrl);
5826
6198
  }
5827
6199
  async dispatch(job, payload) {
5828
6200
  const name = jobRegistry.resolveName(job);
@@ -5848,7 +6220,7 @@ class QueueWorker {
5848
6220
  constructor(redisUrl, failedJobs, timeoutSeconds = 5) {
5849
6221
  this.failedJobs = failedJobs;
5850
6222
  this.timeoutSeconds = timeoutSeconds;
5851
- this.client = new RedisClient4(redisUrl);
6223
+ this.client = new RedisClient5(redisUrl);
5852
6224
  }
5853
6225
  requestStop() {
5854
6226
  this.stopping = true;
@@ -5946,7 +6318,7 @@ function createQueueWorker(redisUrl, failedJobs = createFailedJobService()) {
5946
6318
  return new QueueWorker(redisUrl, failedJobs);
5947
6319
  }
5948
6320
  // ../../src/core/queue/queueMetrics.ts
5949
- var {RedisClient: RedisClient5 } = globalThis.Bun;
6321
+ var {RedisClient: RedisClient6 } = globalThis.Bun;
5950
6322
 
5951
6323
  // ../../src/core/security/safeUrl.ts
5952
6324
  var BLOCKED_HOSTNAMES = new Set([
@@ -5959,7 +6331,7 @@ var BLOCKED_HOSTNAMES = new Set([
5959
6331
 
5960
6332
  // ../../src/core/queue/queueMetrics.ts
5961
6333
  async function readRedisQueueDepth(redisUrl) {
5962
- const client = new RedisClient5(redisUrl);
6334
+ const client = new RedisClient6(redisUrl);
5963
6335
  const [high, defaultQueue, low] = await Promise.all([
5964
6336
  client.llen(QUEUE_HIGH_KEY),
5965
6337
  client.llen(QUEUE_LIST_KEY),
@@ -6452,6 +6824,7 @@ export {
6452
6824
  createCsrfProtection,
6453
6825
  createCsrfMiddleware,
6454
6826
  createCorsMiddleware,
6827
+ createCacheStore,
6455
6828
  createBodySizeLimitMiddleware,
6456
6829
  createAuthorizeMiddleware,
6457
6830
  createAuthMiddleware,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "0.5.15",
3
+ "version": "0.5.16",
4
4
  "description": "Strata — Laravel-inspired Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -90,6 +90,11 @@
90
90
  "import": "./dist/entries/cache/tags.js",
91
91
  "default": "./dist/entries/cache/tags.js"
92
92
  },
93
+ "./cache/createCacheStore": {
94
+ "types": "./dist/core/cache/createCacheStore.d.ts",
95
+ "import": "./dist/entries/cache/createCacheStore.js",
96
+ "default": "./dist/entries/cache/createCacheStore.js"
97
+ },
93
98
  "./crypto/fieldEncryption": {
94
99
  "types": "./dist/core/crypto/fieldEncryption.d.ts",
95
100
  "import": "./dist/entries/crypto/fieldEncryption.js",
@@ -312,7 +317,7 @@
312
317
  "build:bundle": "bun build index.ts --outdir dist --target bun --external bun --external eta",
313
318
  "build:types": "tsc -p tsconfig.types.json",
314
319
  "prepublishOnly": "bun run build && bun ../../scripts/prepare-core-package-publish.ts",
315
- "build:subpaths": "bun build entries/auth/oauth/oidcProvider.ts entries/auth/oauth/providers.ts entries/auth/oauth/samlProvider.ts entries/auth/oauth/types.ts entries/auth/password.ts entries/auth/sessionCookie.ts entries/auth/tokenHash.ts entries/cache/tags.ts entries/crypto/fieldEncryption.ts entries/crypto/mfaSecret.ts entries/database/factory.ts entries/database/seeders.ts entries/database/types.ts entries/errors/http.ts entries/http/contentNegotiation.ts entries/http/csrfToken.ts entries/http/etag.ts entries/http/flashSession.ts entries/http/parseFormBody.ts entries/http/resources.ts entries/http/webErrorResponse.ts entries/http/webFormRequest.ts entries/jobs/dispatchWebhookJob.ts entries/lifecycle/gracefulShutdown.ts entries/metrics/prometheus.ts entries/pagination.ts entries/queue/createAppQueue.ts entries/queue/failedJobService.ts entries/queue/jobRegistry.ts entries/queue/jobRunner.ts entries/queue/queueMetrics.ts entries/queue/publicQueue.ts entries/queue/types.ts entries/security/oauthState.ts entries/security/publicReads.ts entries/security/safeUrl.ts entries/security/stripeWebhook.ts entries/security/tokenExpiry.ts entries/security/totp.ts entries/storage/storage.ts entries/validation/rules.ts entries/view.ts --outdir dist --root . --target bun --external bun --external eta",
320
+ "build:subpaths": "bun build entries/auth/oauth/oidcProvider.ts entries/auth/oauth/providers.ts entries/auth/oauth/samlProvider.ts entries/auth/oauth/types.ts entries/auth/password.ts entries/auth/sessionCookie.ts entries/auth/tokenHash.ts entries/cache/tags.ts entries/cache/createCacheStore.ts entries/crypto/fieldEncryption.ts entries/crypto/mfaSecret.ts entries/database/factory.ts entries/database/seeders.ts entries/database/types.ts entries/errors/http.ts entries/http/contentNegotiation.ts entries/http/csrfToken.ts entries/http/etag.ts entries/http/flashSession.ts entries/http/parseFormBody.ts entries/http/resources.ts entries/http/webErrorResponse.ts entries/http/webFormRequest.ts entries/jobs/dispatchWebhookJob.ts entries/lifecycle/gracefulShutdown.ts entries/metrics/prometheus.ts entries/pagination.ts entries/queue/createAppQueue.ts entries/queue/failedJobService.ts entries/queue/jobRegistry.ts entries/queue/jobRunner.ts entries/queue/queueMetrics.ts entries/queue/publicQueue.ts entries/queue/types.ts entries/security/oauthState.ts entries/security/publicReads.ts entries/security/safeUrl.ts entries/security/stripeWebhook.ts entries/security/tokenExpiry.ts entries/security/totp.ts entries/storage/storage.ts entries/validation/rules.ts entries/view.ts --outdir dist --root . --target bun --external bun --external eta",
316
321
  "build:shims": "bun ../../scripts/write-core-shared-shims.ts"
317
322
  },
318
323
  "publishConfig": {