@chidchanun/bcp 0.2.15 → 0.2.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.
- package/README.md +109 -10
- package/docs/README.md +41 -34
- package/docs/api-manifest.json +4 -4
- package/docs/api-reference.md +62 -3
- package/docs/cache-platform-v2.md +487 -0
- package/docs/docs-web-manifest.json +5 -3
- package/docs/platform-manifest.json +18 -4
- package/docs/releases/0.2.16.md +147 -0
- package/package.json +2 -2
- package/packages/cache/src/platform-v2.ts +1705 -0
- package/packages/client/src/cache.mjs +1554 -0
- package/packages/client/src/cache.ts +29 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
# Cache Platform v2
|
|
2
|
+
|
|
3
|
+
BCP Framework `0.2.16` extends `bcp/cache` with provider-neutral asynchronous cache storage, distributed locking, cache-stampede protection, Redis-compatible adapters and metrics integration while preserving the existing request-cache and `cache()` APIs.
|
|
4
|
+
|
|
5
|
+
## Compatibility
|
|
6
|
+
|
|
7
|
+
The existing APIs remain available and keep their process-local behavior:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
cache,
|
|
12
|
+
dedupe,
|
|
13
|
+
revalidatePath,
|
|
14
|
+
revalidateTag,
|
|
15
|
+
} from "bcp/cache";
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`cache()` is still useful for simple single-process data caching and `dedupe()` is still request-scoped work sharing.
|
|
19
|
+
|
|
20
|
+
Use Cache Platform v2 when cached state must be shared across multiple application instances or when cache fill work needs distributed stampede protection.
|
|
21
|
+
|
|
22
|
+
## Create a cache store
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import {
|
|
26
|
+
createCacheStore,
|
|
27
|
+
} from "bcp/cache";
|
|
28
|
+
|
|
29
|
+
export const applicationCache =
|
|
30
|
+
createCacheStore();
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Without an adapter, the store uses the built-in memory adapter.
|
|
34
|
+
|
|
35
|
+
Basic operations:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
await applicationCache.set(
|
|
39
|
+
"user:42",
|
|
40
|
+
{
|
|
41
|
+
id: 42,
|
|
42
|
+
name: "Bank",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
ttlMs: 60_000,
|
|
46
|
+
tags: ["users"],
|
|
47
|
+
paths: ["/users/42"],
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const user =
|
|
52
|
+
await applicationCache.get(
|
|
53
|
+
"user:42"
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
await applicationCache.delete(
|
|
57
|
+
"user:42"
|
|
58
|
+
);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Cache adapter contract
|
|
62
|
+
|
|
63
|
+
Shared cache providers implement `CacheAdapter`:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
interface CacheAdapter {
|
|
67
|
+
get<T>(key: string):
|
|
68
|
+
Promise<CacheAdapterEntry<T> | null>;
|
|
69
|
+
|
|
70
|
+
set<T>(
|
|
71
|
+
key: string,
|
|
72
|
+
value: T,
|
|
73
|
+
options?: CacheAdapterSetOptions
|
|
74
|
+
): Promise<void>;
|
|
75
|
+
|
|
76
|
+
delete(key: string): Promise<boolean>;
|
|
77
|
+
clear(): Promise<number>;
|
|
78
|
+
revalidateTag(tag: string): Promise<number>;
|
|
79
|
+
revalidatePath(path: string): Promise<number>;
|
|
80
|
+
entries?(): Promise<number>;
|
|
81
|
+
close?(): Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The built-in implementations are:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
createMemoryCacheAdapter()
|
|
89
|
+
createRedisCacheAdapter()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This contract allows another provider to be added without changing application cache usage.
|
|
93
|
+
|
|
94
|
+
## TTL
|
|
95
|
+
|
|
96
|
+
Cache Store v2 uses millisecond TTL values:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
await applicationCache.set(
|
|
100
|
+
"dashboard:summary",
|
|
101
|
+
summary,
|
|
102
|
+
{
|
|
103
|
+
ttlMs: 30_000,
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`ttlMs: false` or an omitted TTL creates an entry without an application-level expiration.
|
|
109
|
+
|
|
110
|
+
`ttlMs: 0` deletes the existing entry instead of storing a new one.
|
|
111
|
+
|
|
112
|
+
The older `cache()` API continues to use `revalidate` in seconds for backward compatibility.
|
|
113
|
+
|
|
114
|
+
## Tag invalidation
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
await applicationCache.set(
|
|
118
|
+
"user:42",
|
|
119
|
+
user,
|
|
120
|
+
{
|
|
121
|
+
tags: [
|
|
122
|
+
"users",
|
|
123
|
+
"tenant:7",
|
|
124
|
+
],
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
await applicationCache.revalidateTag(
|
|
129
|
+
"users"
|
|
130
|
+
);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The returned number is the count of matching entries removed by the adapter.
|
|
134
|
+
|
|
135
|
+
## Path invalidation
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
await applicationCache.set(
|
|
139
|
+
"users-page",
|
|
140
|
+
data,
|
|
141
|
+
{
|
|
142
|
+
paths: [
|
|
143
|
+
"/dashboard/users",
|
|
144
|
+
],
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await applicationCache.revalidatePath(
|
|
149
|
+
"/dashboard"
|
|
150
|
+
);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Path invalidation is hierarchical. Invalidating `/dashboard` also invalidates cached paths below `/dashboard/...`.
|
|
154
|
+
|
|
155
|
+
## `getOrSet()`
|
|
156
|
+
|
|
157
|
+
Use `getOrSet()` for cache-aside loading:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
const user =
|
|
161
|
+
await applicationCache.getOrSet(
|
|
162
|
+
"user:42",
|
|
163
|
+
async () =>
|
|
164
|
+
databaseUser(42),
|
|
165
|
+
{
|
|
166
|
+
ttlMs: 60_000,
|
|
167
|
+
tags: ["users"],
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Within one `CacheStore`, concurrent calls for the same key share one in-flight loader.
|
|
173
|
+
|
|
174
|
+
```text
|
|
175
|
+
Request A ----\
|
|
176
|
+
Request B -----+--> one loader --> cache
|
|
177
|
+
Request C ----/
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
This local singleflight behavior is available even without a distributed lock adapter.
|
|
181
|
+
|
|
182
|
+
## Distributed stampede protection
|
|
183
|
+
|
|
184
|
+
For multiple application instances, provide a shared `CacheLockAdapter`:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const applicationCache =
|
|
188
|
+
createCacheStore({
|
|
189
|
+
adapter: redisCache,
|
|
190
|
+
lock: redisLock,
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The load flow becomes:
|
|
195
|
+
|
|
196
|
+
```text
|
|
197
|
+
Instance A cache miss
|
|
198
|
+
|
|
|
199
|
+
+--> acquire distributed lock
|
|
200
|
+
| |
|
|
201
|
+
| +--> double-check cache
|
|
202
|
+
| +--> execute loader
|
|
203
|
+
| +--> store result
|
|
204
|
+
| +--> release lock
|
|
205
|
+
|
|
|
206
|
+
Instance B cache miss
|
|
207
|
+
|
|
|
208
|
+
+--> lock contention
|
|
209
|
+
|
|
|
210
|
+
+--> poll shared cache
|
|
211
|
+
+--> consume A's result
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Default values:
|
|
215
|
+
|
|
216
|
+
```text
|
|
217
|
+
lockTtlMs 30000
|
|
218
|
+
waitTimeoutMs 5000
|
|
219
|
+
pollIntervalMs 25
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Override them per operation:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
await applicationCache.getOrSet(
|
|
226
|
+
"report:monthly",
|
|
227
|
+
buildReport,
|
|
228
|
+
{
|
|
229
|
+
lockTtlMs: 60_000,
|
|
230
|
+
waitTimeoutMs: 10_000,
|
|
231
|
+
pollIntervalMs: 50,
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Lock heartbeat
|
|
237
|
+
|
|
238
|
+
If the selected lock adapter implements `extend()`, BCP renews the acquired load lock while the loader is still active. The renewal interval is approximately one third of `lockTtlMs`.
|
|
239
|
+
|
|
240
|
+
This reduces duplicate work when a legitimate load takes longer than the original lease.
|
|
241
|
+
|
|
242
|
+
A process crash can still let the lease expire, which is intentional: another process must eventually be able to recover and refill the cache.
|
|
243
|
+
|
|
244
|
+
## Lock timeout policy
|
|
245
|
+
|
|
246
|
+
The default timeout behavior is availability-oriented. If a process waits for another lock owner but still cannot acquire the lock after the timeout, it executes the loader rather than blocking indefinitely.
|
|
247
|
+
|
|
248
|
+
For workloads where duplicate computation is worse than a failed request:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
await applicationCache.getOrSet(
|
|
252
|
+
"expensive-model",
|
|
253
|
+
buildModel,
|
|
254
|
+
{
|
|
255
|
+
onLockTimeout: "error",
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
This throws instead of running an unlocked fallback loader.
|
|
261
|
+
|
|
262
|
+
## Memory lock adapter
|
|
263
|
+
|
|
264
|
+
Tests and single-process deployments can use:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
import {
|
|
268
|
+
createMemoryCacheAdapter,
|
|
269
|
+
createMemoryCacheLockAdapter,
|
|
270
|
+
} from "bcp/cache";
|
|
271
|
+
|
|
272
|
+
const adapter =
|
|
273
|
+
createMemoryCacheAdapter();
|
|
274
|
+
const lock =
|
|
275
|
+
createMemoryCacheLockAdapter();
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Multiple `CacheStore` instances can share these objects during tests to exercise cross-store contention behavior deterministically.
|
|
279
|
+
|
|
280
|
+
## Redis-compatible cache adapter
|
|
281
|
+
|
|
282
|
+
BCP does not install or own a Redis library.
|
|
283
|
+
|
|
284
|
+
Provide a minimal command client:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
interface RedisCacheCommandClient {
|
|
288
|
+
sendCommand(
|
|
289
|
+
command: string[]
|
|
290
|
+
): Promise<unknown>;
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Example adapter wiring:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
import {
|
|
298
|
+
createCacheStore,
|
|
299
|
+
createRedisCacheAdapter,
|
|
300
|
+
createRedisCacheLockAdapter,
|
|
301
|
+
} from "bcp/cache";
|
|
302
|
+
|
|
303
|
+
const redisCache =
|
|
304
|
+
createRedisCacheAdapter({
|
|
305
|
+
client: redisClient,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const redisLock =
|
|
309
|
+
createRedisCacheLockAdapter({
|
|
310
|
+
client: redisClient,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
export const cache =
|
|
314
|
+
createCacheStore({
|
|
315
|
+
adapter: redisCache,
|
|
316
|
+
lock: redisLock,
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The default namespace is:
|
|
321
|
+
|
|
322
|
+
```text
|
|
323
|
+
bcp:{cache}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The `{cache}` hash tag keeps the reference adapter's related keys in one Redis Cluster slot for its Lua state transitions.
|
|
327
|
+
|
|
328
|
+
When overriding the namespace for Redis Cluster, keep a hash tag, for example:
|
|
329
|
+
|
|
330
|
+
```text
|
|
331
|
+
my-app:{cache}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Redis value serialization
|
|
335
|
+
|
|
336
|
+
The reference Redis adapter stores serialized values inside cache records. The default serializer is JSON.
|
|
337
|
+
|
|
338
|
+
Use custom serialization for values that JSON cannot safely represent:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
createRedisCacheAdapter({
|
|
342
|
+
client: redisClient,
|
|
343
|
+
serialize(value) {
|
|
344
|
+
return customEncode(value);
|
|
345
|
+
},
|
|
346
|
+
deserialize(value) {
|
|
347
|
+
return customDecode(value);
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The serializer must return a string.
|
|
353
|
+
|
|
354
|
+
## Redis connection ownership
|
|
355
|
+
|
|
356
|
+
BCP never creates a Redis connection. The application owns:
|
|
357
|
+
|
|
358
|
+
- connection URL and credentials
|
|
359
|
+
- TLS
|
|
360
|
+
- Sentinel/Cluster configuration
|
|
361
|
+
- reconnect behavior
|
|
362
|
+
- process shutdown
|
|
363
|
+
|
|
364
|
+
An adapter can receive a close hook:
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
createRedisCacheAdapter({
|
|
368
|
+
client: redisClient,
|
|
369
|
+
close: async () => {
|
|
370
|
+
await redisClient.quit();
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
If the cache and lock adapters share one Redis connection, avoid registering two independent close hooks for that same connection. Prefer application-owned coordinated shutdown.
|
|
376
|
+
|
|
377
|
+
## Redis atomic transitions
|
|
378
|
+
|
|
379
|
+
The reference cache adapter uses Lua for cache record/index mutations, tag invalidation and distributed lock compare-and-release / compare-and-renew operations.
|
|
380
|
+
|
|
381
|
+
This prevents one lock owner from releasing or renewing a lock that has already expired and been acquired by another owner.
|
|
382
|
+
|
|
383
|
+
Path invalidation first discovers registered path indexes and then performs a Lua cleanup over the matching sets. For extremely large path catalogs, a specialized provider can implement a more scalable indexing strategy behind the same `CacheAdapter` contract.
|
|
384
|
+
|
|
385
|
+
## Metrics integration
|
|
386
|
+
|
|
387
|
+
Cache Platform v2 can emit metrics to the existing BCP metrics registry:
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
import {
|
|
391
|
+
createCacheMetrics,
|
|
392
|
+
createCacheStore,
|
|
393
|
+
} from "bcp/cache";
|
|
394
|
+
import {
|
|
395
|
+
createMetricsRegistry,
|
|
396
|
+
} from "bcp/observability";
|
|
397
|
+
|
|
398
|
+
const metrics =
|
|
399
|
+
createMetricsRegistry();
|
|
400
|
+
|
|
401
|
+
const cache =
|
|
402
|
+
createCacheStore({
|
|
403
|
+
metrics:
|
|
404
|
+
createCacheMetrics(
|
|
405
|
+
metrics
|
|
406
|
+
),
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
The default metrics are:
|
|
411
|
+
|
|
412
|
+
```text
|
|
413
|
+
bcp_cache_operations_total{event="..."}
|
|
414
|
+
bcp_cache_in_flight
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Events include hits, misses, sets, deletes, invalidations, loader outcomes and lock contention/timeout activity.
|
|
418
|
+
|
|
419
|
+
Use a custom prefix when multiple logical caches need separate metric families:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
createCacheMetrics(
|
|
423
|
+
metrics,
|
|
424
|
+
{
|
|
425
|
+
prefix:
|
|
426
|
+
"bcp_session_cache",
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## Store statistics
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
const stats =
|
|
435
|
+
await cache.stats();
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
The snapshot includes:
|
|
439
|
+
|
|
440
|
+
```text
|
|
441
|
+
hits
|
|
442
|
+
misses
|
|
443
|
+
sets
|
|
444
|
+
deletes
|
|
445
|
+
invalidations
|
|
446
|
+
loads
|
|
447
|
+
loadErrors
|
|
448
|
+
lockAcquired
|
|
449
|
+
lockContentions
|
|
450
|
+
lockTimeouts
|
|
451
|
+
inFlight
|
|
452
|
+
adapterEntries (when supported)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Statistics belong to the `CacheStore` process. `adapterEntries` can come from shared storage when the provider supports it.
|
|
456
|
+
|
|
457
|
+
## Failure model
|
|
458
|
+
|
|
459
|
+
A cache is an optimization, not the system of record.
|
|
460
|
+
|
|
461
|
+
Recommended production rules:
|
|
462
|
+
|
|
463
|
+
1. Keep durable application truth in the database/object store.
|
|
464
|
+
2. Design loaders so cache loss can be recovered by recomputation.
|
|
465
|
+
3. Choose `onLockTimeout` based on whether availability or duplicate-work avoidance matters more.
|
|
466
|
+
4. Do not use distributed cache locks as a replacement for database transaction/uniqueness rules protecting business invariants.
|
|
467
|
+
5. Treat Redis/cache outages separately from durable queues, workflows and outbox guarantees.
|
|
468
|
+
|
|
469
|
+
## Closing the store
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
await cache.close();
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
`close()` clears local in-flight bookkeeping and calls configured adapter/lock lifecycle hooks.
|
|
476
|
+
|
|
477
|
+
## Published runtime
|
|
478
|
+
|
|
479
|
+
`0.2.16` prepares `bcp/cache` as compiled `cache.mjs` for the npm package. TypeScript source remains the type surface, while standalone Node imports execute the compiled runtime.
|
|
480
|
+
|
|
481
|
+
## Related guides
|
|
482
|
+
|
|
483
|
+
- [Caching](caching.md)
|
|
484
|
+
- [Observability Platform v2](observability.md)
|
|
485
|
+
- [Plugin & Module Platform](plugin-module-platform.md)
|
|
486
|
+
- [Testing Platform](testing-platform.md)
|
|
487
|
+
- [Production Hardening](production-hardening.md)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"framework": "bcp",
|
|
4
|
-
"versionTarget": "0.2.
|
|
4
|
+
"versionTarget": "0.2.16",
|
|
5
5
|
"releaseState": "unreleased",
|
|
6
6
|
"sections": [
|
|
7
7
|
{
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
{
|
|
59
59
|
"id": "runtime",
|
|
60
60
|
"title": "Runtime & Infrastructure",
|
|
61
|
-
"description": "Middleware, jobs, scheduling, workflows, event delivery, realtime channels, observability, caching, security and production hardening.",
|
|
61
|
+
"description": "Middleware, jobs, scheduling, workflows, event delivery, realtime channels, observability, distributed caching, security and production hardening.",
|
|
62
62
|
"pages": [
|
|
63
63
|
{ "route": "/docs/middleware", "source": "middleware.md", "title": "Middleware" },
|
|
64
64
|
{ "route": "/docs/hydration", "source": "hydration.md", "title": "Hydration" },
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
{ "route": "/docs/workflow-orchestration", "source": "workflow-orchestration.md", "title": "Workflow Orchestration" },
|
|
71
71
|
{ "route": "/docs/realtime-platform", "source": "realtime-platform.md", "title": "Realtime Platform" },
|
|
72
72
|
{ "route": "/docs/caching", "source": "caching.md", "title": "Caching" },
|
|
73
|
+
{ "route": "/docs/cache-platform-v2", "source": "cache-platform-v2.md", "title": "Cache Platform v2" },
|
|
73
74
|
{ "route": "/docs/security", "source": "security.md", "title": "Security" },
|
|
74
75
|
{ "route": "/docs/production-hardening", "source": "production-hardening.md", "title": "Production Hardening" }
|
|
75
76
|
]
|
|
@@ -116,7 +117,8 @@
|
|
|
116
117
|
}
|
|
117
118
|
],
|
|
118
119
|
"releases": [
|
|
119
|
-
{ "route": "/releases/0.2.
|
|
120
|
+
{ "route": "/releases/0.2.16", "source": "releases/0.2.16.md", "version": "0.2.16", "state": "unreleased" },
|
|
121
|
+
{ "route": "/releases/0.2.15", "source": "releases/0.2.15.md", "version": "0.2.15" },
|
|
120
122
|
{ "route": "/releases/0.2.14", "source": "releases/0.2.14.md", "version": "0.2.14" },
|
|
121
123
|
{ "route": "/releases/0.2.13", "source": "releases/0.2.13.md", "version": "0.2.13" },
|
|
122
124
|
{ "route": "/releases/0.2.12", "source": "releases/0.2.12.md", "version": "0.2.12" },
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"framework": "bcp",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.16",
|
|
5
5
|
"releaseState": "unreleased",
|
|
6
|
-
"baseline": "
|
|
6
|
+
"baseline": "cache-platform-v2",
|
|
7
7
|
"runtime": {
|
|
8
8
|
"node": ">=24.11.0",
|
|
9
9
|
"react": "19",
|
|
@@ -148,6 +148,19 @@
|
|
|
148
148
|
"pluginServiceRegistry": true,
|
|
149
149
|
"pluginHookBus": true,
|
|
150
150
|
"pluginLifecycleRollback": true,
|
|
151
|
+
"cachePlatformV2": true,
|
|
152
|
+
"cacheAdapterContract": true,
|
|
153
|
+
"memoryCacheAdapter": true,
|
|
154
|
+
"redisCacheAdapter": true,
|
|
155
|
+
"cacheLockAdapter": true,
|
|
156
|
+
"redisCacheLockAdapter": true,
|
|
157
|
+
"cacheStampedeProtection": true,
|
|
158
|
+
"cacheLockHeartbeats": true,
|
|
159
|
+
"cacheTtl": true,
|
|
160
|
+
"cacheTagInvalidation": true,
|
|
161
|
+
"cachePathInvalidation": true,
|
|
162
|
+
"cacheMetrics": true,
|
|
163
|
+
"compiledCacheRuntime": true,
|
|
151
164
|
"databaseMigrations": true,
|
|
152
165
|
"databaseAdapterContract": true,
|
|
153
166
|
"databasePostgresql": true,
|
|
@@ -187,7 +200,7 @@
|
|
|
187
200
|
"s3-compatible"
|
|
188
201
|
],
|
|
189
202
|
"compatibility": {
|
|
190
|
-
"previousBaseline": "0.2.
|
|
203
|
+
"previousBaseline": "0.2.15",
|
|
191
204
|
"intentionalBreakingChangesFromPreviousBaseline": false,
|
|
192
205
|
"migrationGuide": "migration-0.2.md"
|
|
193
206
|
},
|
|
@@ -212,7 +225,8 @@
|
|
|
212
225
|
"realtimePlatform": "realtime-platform.md",
|
|
213
226
|
"testingPlatform": "testing-platform.md",
|
|
214
227
|
"pluginModulePlatform": "plugin-module-platform.md",
|
|
228
|
+
"cachePlatformV2": "cache-platform-v2.md",
|
|
215
229
|
"migrationGuide": "migration-0.2.md",
|
|
216
|
-
"releaseNotes": "releases/0.2.
|
|
230
|
+
"releaseNotes": "releases/0.2.16.md"
|
|
217
231
|
}
|
|
218
232
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# BCP Framework 0.2.16
|
|
2
|
+
|
|
3
|
+
**State:** unreleased
|
|
4
|
+
|
|
5
|
+
## Cache Platform v2
|
|
6
|
+
|
|
7
|
+
`0.2.16` upgrades the existing `bcp/cache` public entrypoint with provider-neutral asynchronous cache storage, distributed lock contracts, Redis-compatible reference adapters, cache-stampede protection and metrics integration.
|
|
8
|
+
|
|
9
|
+
The existing `cache()`, `dedupe()`, `revalidateTag()` and `revalidatePath()` APIs remain available with their previous process-local behavior.
|
|
10
|
+
|
|
11
|
+
## New public APIs
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
createCacheStore()
|
|
15
|
+
createMemoryCacheAdapter()
|
|
16
|
+
createMemoryCacheLockAdapter()
|
|
17
|
+
createRedisCacheAdapter()
|
|
18
|
+
createRedisCacheLockAdapter()
|
|
19
|
+
createCacheMetrics()
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
New public contracts include:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
CacheAdapter
|
|
26
|
+
CacheAdapterEntry
|
|
27
|
+
CacheAdapterSetOptions
|
|
28
|
+
CacheLockAdapter
|
|
29
|
+
CacheStore
|
|
30
|
+
CacheStoreOptions
|
|
31
|
+
CacheStoreSetOptions
|
|
32
|
+
CacheGetOrSetOptions
|
|
33
|
+
CacheStoreStats
|
|
34
|
+
CacheMetricsSink
|
|
35
|
+
CacheMetricsRegistryLike
|
|
36
|
+
RedisCacheCommandClient
|
|
37
|
+
RedisCacheAdapterOptions
|
|
38
|
+
RedisCacheLockAdapterOptions
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Cache-aside loading
|
|
42
|
+
|
|
43
|
+
`CacheStore.getOrSet()` provides a framework-native cache-aside primitive.
|
|
44
|
+
|
|
45
|
+
Within one process it deduplicates concurrent loaders for the same key. With a shared `CacheLockAdapter`, multiple BCP instances coordinate cache fills through distributed leases.
|
|
46
|
+
|
|
47
|
+
## Distributed lock behavior
|
|
48
|
+
|
|
49
|
+
The built-in lock implementations support:
|
|
50
|
+
|
|
51
|
+
- acquire with owner identity and TTL
|
|
52
|
+
- compare-and-release
|
|
53
|
+
- optional compare-and-extend
|
|
54
|
+
- heartbeat renewal while a loader is active
|
|
55
|
+
- contention wait/poll against shared cache state
|
|
56
|
+
- explicit timeout failure with `onLockTimeout: "error"`
|
|
57
|
+
- availability-oriented unlocked fallback by default after timeout
|
|
58
|
+
|
|
59
|
+
Distributed locks reduce duplicate cache fill work. They are not a substitute for database transactions or uniqueness constraints protecting business invariants.
|
|
60
|
+
|
|
61
|
+
## Redis-compatible adapters
|
|
62
|
+
|
|
63
|
+
BCP still does not depend on a Redis package.
|
|
64
|
+
|
|
65
|
+
Applications provide a minimal command client:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
interface RedisCacheCommandClient {
|
|
69
|
+
sendCommand(
|
|
70
|
+
command: string[]
|
|
71
|
+
): Promise<unknown>;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The default namespace is `bcp:{cache}` so related keys share a Redis Cluster hash slot.
|
|
76
|
+
|
|
77
|
+
The reference adapter uses Lua for record/index changes, tag invalidation, lock release and lock renewal.
|
|
78
|
+
|
|
79
|
+
Applications remain responsible for Redis authentication, TLS, Cluster/Sentinel configuration, reconnect behavior and connection shutdown.
|
|
80
|
+
|
|
81
|
+
## TTL and invalidation
|
|
82
|
+
|
|
83
|
+
Cache Store v2 supports:
|
|
84
|
+
|
|
85
|
+
- millisecond TTL via `ttlMs`
|
|
86
|
+
- tag invalidation
|
|
87
|
+
- hierarchical path invalidation
|
|
88
|
+
- adapter-wide clear
|
|
89
|
+
- optional provider entry counts
|
|
90
|
+
|
|
91
|
+
The older `cache()` API continues to accept `revalidate` in seconds for compatibility.
|
|
92
|
+
|
|
93
|
+
## Observability
|
|
94
|
+
|
|
95
|
+
`createCacheMetrics()` adapts Cache Store events to `bcp/observability`'s existing `MetricsRegistry` contract.
|
|
96
|
+
|
|
97
|
+
Default metric families:
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
bcp_cache_operations_total{event="..."}
|
|
101
|
+
bcp_cache_in_flight
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Published runtime
|
|
105
|
+
|
|
106
|
+
The prepared npm package now compiles `bcp/cache` to:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
packages/client/src/cache.mjs
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The prepared package export points runtime imports at this compiled file while preserving `cache.ts` as the public type source.
|
|
113
|
+
|
|
114
|
+
## Testing
|
|
115
|
+
|
|
116
|
+
The `0.2.16` suite adds coverage for:
|
|
117
|
+
|
|
118
|
+
- TTL expiration
|
|
119
|
+
- tag/path invalidation
|
|
120
|
+
- store statistics
|
|
121
|
+
- local in-flight deduplication
|
|
122
|
+
- cross-store distributed stampede protection
|
|
123
|
+
- lock timeout errors
|
|
124
|
+
- BCP metrics registry integration
|
|
125
|
+
- Redis command/namespace contract
|
|
126
|
+
- compiled prepared-package runtime smoke
|
|
127
|
+
|
|
128
|
+
## Compatibility
|
|
129
|
+
|
|
130
|
+
`0.2.16` has no intentional breaking changes from `0.2.15`.
|
|
131
|
+
|
|
132
|
+
Existing application calls using the original cache APIs do not need to migrate.
|
|
133
|
+
|
|
134
|
+
## Release validation
|
|
135
|
+
|
|
136
|
+
Before publishing:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npm run typecheck
|
|
140
|
+
npm run test:unit
|
|
141
|
+
npm run test:integration
|
|
142
|
+
npm run test:e2e
|
|
143
|
+
npm run test:package
|
|
144
|
+
npm run rc:check
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Tag and publish only the exact commit that passes the complete RC sequence.
|