@open-mercato/cache 0.6.6-develop.6494.1.8479db6c38 → 0.6.6-develop.6506.1.699ffb5533
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/dist/tenantContext.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
const
|
|
2
|
+
const TENANT_STORAGE_KEY = /* @__PURE__ */ Symbol.for("@open-mercato/cache/tenant-context");
|
|
3
|
+
function resolveTenantStorage() {
|
|
4
|
+
const scope = globalThis;
|
|
5
|
+
if (!scope[TENANT_STORAGE_KEY]) {
|
|
6
|
+
scope[TENANT_STORAGE_KEY] = new AsyncLocalStorage();
|
|
7
|
+
}
|
|
8
|
+
return scope[TENANT_STORAGE_KEY];
|
|
9
|
+
}
|
|
10
|
+
const tenantStorage = resolveTenantStorage();
|
|
3
11
|
function runWithCacheTenant(tenantId, fn) {
|
|
4
12
|
return tenantStorage.run(tenantId ?? null, fn);
|
|
5
13
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/tenantContext.ts"],
|
|
4
|
-
"sourcesContent": ["import { AsyncLocalStorage } from 'node:async_hooks'\n\nconst
|
|
5
|
-
"mappings": "AAAA,SAAS,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import { AsyncLocalStorage } from 'node:async_hooks'\n\n// Bundlers (Turbopack/esbuild/tsx) can emit this module into multiple chunks,\n// each with its own module-local AsyncLocalStorage. A tenant context entered\n// through one copy (e.g. an API route's `runWithCacheTenant`) is then invisible\n// to the copy the cache service's tenant-aware wrapper reads, so entries get\n// stored under the `tenant:global:` prefix while invalidations that DO carry\n// the tenant (queue workers, cross-process drains) target `tenant:<id>:` \u2014\n// tag-based eviction silently misses and reads stay stale until TTL.\n// Key the storage off globalThis via Symbol.for so every duplicated copy\n// resolves the same instance. Mirrors the data_sync adapter-registry fix.\nconst TENANT_STORAGE_KEY = Symbol.for('@open-mercato/cache/tenant-context')\n\ntype GlobalWithTenantStorage = typeof globalThis & {\n [TENANT_STORAGE_KEY]?: AsyncLocalStorage<string | null>\n}\n\nfunction resolveTenantStorage(): AsyncLocalStorage<string | null> {\n const scope = globalThis as GlobalWithTenantStorage\n if (!scope[TENANT_STORAGE_KEY]) {\n scope[TENANT_STORAGE_KEY] = new AsyncLocalStorage<string | null>()\n }\n return scope[TENANT_STORAGE_KEY]\n}\n\nconst tenantStorage = resolveTenantStorage()\n\nexport function runWithCacheTenant<T>(tenantId: string | null, fn: () => T): T\nexport function runWithCacheTenant<T>(tenantId: string | null, fn: () => Promise<T>): Promise<T>\nexport function runWithCacheTenant<T>(tenantId: string | null, fn: () => T | Promise<T>): T | Promise<T> {\n return tenantStorage.run(tenantId ?? null, fn)\n}\n\nexport function getCurrentCacheTenant(): string | null {\n return tenantStorage.getStore() ?? null\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,yBAAyB;AAWlC,MAAM,qBAAqB,uBAAO,IAAI,oCAAoC;AAM1E,SAAS,uBAAyD;AAChE,QAAM,QAAQ;AACd,MAAI,CAAC,MAAM,kBAAkB,GAAG;AAC9B,UAAM,kBAAkB,IAAI,IAAI,kBAAiC;AAAA,EACnE;AACA,SAAO,MAAM,kBAAkB;AACjC;AAEA,MAAM,gBAAgB,qBAAqB;AAIpC,SAAS,mBAAsB,UAAyB,IAA0C;AACvG,SAAO,cAAc,IAAI,YAAY,MAAM,EAAE;AAC/C;AAEO,SAAS,wBAAuC;AACrD,SAAO,cAAc,SAAS,KAAK;AACrC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const TENANT_STORAGE_KEY = Symbol.for('@open-mercato/cache/tenant-context')
|
|
2
|
+
|
|
3
|
+
function clearGlobalStorage(): void {
|
|
4
|
+
delete (globalThis as Record<symbol, unknown>)[TENANT_STORAGE_KEY]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
describe('cache tenant context', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
clearGlobalStorage()
|
|
10
|
+
jest.resetModules()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
clearGlobalStorage()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('exposes the tenant inside runWithCacheTenant and null outside', () => {
|
|
18
|
+
const { runWithCacheTenant, getCurrentCacheTenant } = require('../tenantContext')
|
|
19
|
+
expect(getCurrentCacheTenant()).toBeNull()
|
|
20
|
+
runWithCacheTenant('tenant-a', () => {
|
|
21
|
+
expect(getCurrentCacheTenant()).toBe('tenant-a')
|
|
22
|
+
})
|
|
23
|
+
expect(getCurrentCacheTenant()).toBeNull()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('shares the storage across duplicated module copies via globalThis', () => {
|
|
27
|
+
// Simulate a bundler emitting the module into two chunks: two separate
|
|
28
|
+
// module instances must still observe one shared AsyncLocalStorage,
|
|
29
|
+
// otherwise a tenant entered through one copy (an API route) is invisible
|
|
30
|
+
// to the other (the cache service wrapper) and entries get stored under
|
|
31
|
+
// the tenant:global prefix that tenant-scoped invalidations never target.
|
|
32
|
+
const copyA = require('../tenantContext')
|
|
33
|
+
jest.resetModules()
|
|
34
|
+
const copyB = require('../tenantContext')
|
|
35
|
+
expect(copyB).not.toBe(copyA)
|
|
36
|
+
|
|
37
|
+
copyA.runWithCacheTenant('tenant-shared', () => {
|
|
38
|
+
expect(copyB.getCurrentCacheTenant()).toBe('tenant-shared')
|
|
39
|
+
})
|
|
40
|
+
expect(copyB.getCurrentCacheTenant()).toBeNull()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('nested contexts restore the outer tenant', () => {
|
|
44
|
+
const { runWithCacheTenant, getCurrentCacheTenant } = require('../tenantContext')
|
|
45
|
+
runWithCacheTenant('outer', () => {
|
|
46
|
+
runWithCacheTenant('inner', () => {
|
|
47
|
+
expect(getCurrentCacheTenant()).toBe('inner')
|
|
48
|
+
})
|
|
49
|
+
expect(getCurrentCacheTenant()).toBe('outer')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
})
|
package/src/tenantContext.ts
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Bundlers (Turbopack/esbuild/tsx) can emit this module into multiple chunks,
|
|
4
|
+
// each with its own module-local AsyncLocalStorage. A tenant context entered
|
|
5
|
+
// through one copy (e.g. an API route's `runWithCacheTenant`) is then invisible
|
|
6
|
+
// to the copy the cache service's tenant-aware wrapper reads, so entries get
|
|
7
|
+
// stored under the `tenant:global:` prefix while invalidations that DO carry
|
|
8
|
+
// the tenant (queue workers, cross-process drains) target `tenant:<id>:` —
|
|
9
|
+
// tag-based eviction silently misses and reads stay stale until TTL.
|
|
10
|
+
// Key the storage off globalThis via Symbol.for so every duplicated copy
|
|
11
|
+
// resolves the same instance. Mirrors the data_sync adapter-registry fix.
|
|
12
|
+
const TENANT_STORAGE_KEY = Symbol.for('@open-mercato/cache/tenant-context')
|
|
13
|
+
|
|
14
|
+
type GlobalWithTenantStorage = typeof globalThis & {
|
|
15
|
+
[TENANT_STORAGE_KEY]?: AsyncLocalStorage<string | null>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveTenantStorage(): AsyncLocalStorage<string | null> {
|
|
19
|
+
const scope = globalThis as GlobalWithTenantStorage
|
|
20
|
+
if (!scope[TENANT_STORAGE_KEY]) {
|
|
21
|
+
scope[TENANT_STORAGE_KEY] = new AsyncLocalStorage<string | null>()
|
|
22
|
+
}
|
|
23
|
+
return scope[TENANT_STORAGE_KEY]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const tenantStorage = resolveTenantStorage()
|
|
4
27
|
|
|
5
28
|
export function runWithCacheTenant<T>(tenantId: string | null, fn: () => T): T
|
|
6
29
|
export function runWithCacheTenant<T>(tenantId: string | null, fn: () => Promise<T>): Promise<T>
|