@teleporthq/teleport-shared 0.43.53 → 0.43.57
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/__tests__/_helpers/emitted-module-harness.ts +122 -0
- package/__tests__/cache/client-runtime.test.ts +341 -0
- package/__tests__/cache/server-runtime.test.ts +369 -0
- package/__tests__/utils/ast-scope.ts +120 -0
- package/__tests__/utils/ast-statement-order.ts +351 -0
- package/dist/cjs/cache/api-routes.d.ts +27 -0
- package/dist/cjs/cache/api-routes.d.ts.map +1 -0
- package/dist/cjs/cache/api-routes.js +33 -0
- package/dist/cjs/cache/api-routes.js.map +1 -0
- package/dist/cjs/cache/client-runtime.d.ts +41 -0
- package/dist/cjs/cache/client-runtime.d.ts.map +1 -0
- package/dist/cjs/cache/client-runtime.js +48 -0
- package/dist/cjs/cache/client-runtime.js.map +1 -0
- package/dist/cjs/cache/constants.d.ts +62 -0
- package/dist/cjs/cache/constants.d.ts.map +1 -0
- package/dist/cjs/cache/constants.js +69 -0
- package/dist/cjs/cache/constants.js.map +1 -0
- package/dist/cjs/cache/index.d.ts +6 -0
- package/dist/cjs/cache/index.d.ts.map +1 -0
- package/dist/cjs/cache/index.js +22 -0
- package/dist/cjs/cache/index.js.map +1 -0
- package/dist/cjs/cache/server-runtime.d.ts +33 -0
- package/dist/cjs/cache/server-runtime.d.ts.map +1 -0
- package/dist/cjs/cache/server-runtime.js +40 -0
- package/dist/cjs/cache/server-runtime.js.map +1 -0
- package/dist/cjs/cache/version-store.d.ts +28 -0
- package/dist/cjs/cache/version-store.d.ts.map +1 -0
- package/dist/cjs/cache/version-store.js +37 -0
- package/dist/cjs/cache/version-store.js.map +1 -0
- package/dist/cjs/index.d.ts +8 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +15 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils/ast-scope.d.ts +46 -0
- package/dist/cjs/utils/ast-scope.d.ts.map +1 -0
- package/dist/cjs/utils/ast-scope.js +482 -0
- package/dist/cjs/utils/ast-scope.js.map +1 -0
- package/dist/cjs/utils/ast-statement-order.d.ts +103 -0
- package/dist/cjs/utils/ast-statement-order.d.ts.map +1 -0
- package/dist/cjs/utils/ast-statement-order.js +312 -0
- package/dist/cjs/utils/ast-statement-order.js.map +1 -0
- package/dist/cjs/utils/email-date.d.ts +28 -0
- package/dist/cjs/utils/email-date.d.ts.map +1 -0
- package/dist/cjs/utils/email-date.js +35 -0
- package/dist/cjs/utils/email-date.js.map +1 -0
- package/dist/cjs/utils/html-content-model.d.ts +62 -0
- package/dist/cjs/utils/html-content-model.d.ts.map +1 -0
- package/dist/cjs/utils/html-content-model.js +293 -0
- package/dist/cjs/utils/html-content-model.js.map +1 -0
- package/dist/cjs/utils/storefront-tax.d.ts +59 -0
- package/dist/cjs/utils/storefront-tax.d.ts.map +1 -0
- package/dist/cjs/utils/storefront-tax.js +80 -0
- package/dist/cjs/utils/storefront-tax.js.map +1 -0
- package/dist/cjs/utils/style-declarations.d.ts +34 -0
- package/dist/cjs/utils/style-declarations.d.ts.map +1 -0
- package/dist/cjs/utils/style-declarations.js +77 -0
- package/dist/cjs/utils/style-declarations.js.map +1 -0
- package/dist/esm/cache/api-routes.d.ts +27 -0
- package/dist/esm/cache/api-routes.d.ts.map +1 -0
- package/dist/esm/cache/api-routes.js +28 -0
- package/dist/esm/cache/api-routes.js.map +1 -0
- package/dist/esm/cache/client-runtime.d.ts +41 -0
- package/dist/esm/cache/client-runtime.d.ts.map +1 -0
- package/dist/esm/cache/client-runtime.js +44 -0
- package/dist/esm/cache/client-runtime.js.map +1 -0
- package/dist/esm/cache/constants.d.ts +62 -0
- package/dist/esm/cache/constants.d.ts.map +1 -0
- package/dist/esm/cache/constants.js +64 -0
- package/dist/esm/cache/constants.js.map +1 -0
- package/dist/esm/cache/index.d.ts +6 -0
- package/dist/esm/cache/index.d.ts.map +1 -0
- package/dist/esm/cache/index.js +6 -0
- package/dist/esm/cache/index.js.map +1 -0
- package/dist/esm/cache/server-runtime.d.ts +33 -0
- package/dist/esm/cache/server-runtime.d.ts.map +1 -0
- package/dist/esm/cache/server-runtime.js +36 -0
- package/dist/esm/cache/server-runtime.js.map +1 -0
- package/dist/esm/cache/version-store.d.ts +28 -0
- package/dist/esm/cache/version-store.d.ts.map +1 -0
- package/dist/esm/cache/version-store.js +33 -0
- package/dist/esm/cache/version-store.js.map +1 -0
- package/dist/esm/index.d.ts +8 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +8 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils/ast-scope.d.ts +46 -0
- package/dist/esm/utils/ast-scope.d.ts.map +1 -0
- package/dist/esm/utils/ast-scope.js +477 -0
- package/dist/esm/utils/ast-scope.js.map +1 -0
- package/dist/esm/utils/ast-statement-order.d.ts +103 -0
- package/dist/esm/utils/ast-statement-order.d.ts.map +1 -0
- package/dist/esm/utils/ast-statement-order.js +302 -0
- package/dist/esm/utils/ast-statement-order.js.map +1 -0
- package/dist/esm/utils/email-date.d.ts +28 -0
- package/dist/esm/utils/email-date.d.ts.map +1 -0
- package/dist/esm/utils/email-date.js +31 -0
- package/dist/esm/utils/email-date.js.map +1 -0
- package/dist/esm/utils/html-content-model.d.ts +62 -0
- package/dist/esm/utils/html-content-model.d.ts.map +1 -0
- package/dist/esm/utils/html-content-model.js +284 -0
- package/dist/esm/utils/html-content-model.js.map +1 -0
- package/dist/esm/utils/storefront-tax.d.ts +59 -0
- package/dist/esm/utils/storefront-tax.d.ts.map +1 -0
- package/dist/esm/utils/storefront-tax.js +74 -0
- package/dist/esm/utils/storefront-tax.js.map +1 -0
- package/dist/esm/utils/style-declarations.d.ts +34 -0
- package/dist/esm/utils/style-declarations.d.ts.map +1 -0
- package/dist/esm/utils/style-declarations.js +73 -0
- package/dist/esm/utils/style-declarations.js.map +1 -0
- package/package.json +3 -3
- package/src/cache/api-routes.ts +93 -0
- package/src/cache/client-runtime.ts +416 -0
- package/src/cache/constants.ts +74 -0
- package/src/cache/index.ts +5 -0
- package/src/cache/server-runtime.ts +251 -0
- package/src/cache/version-store.ts +137 -0
- package/src/index.ts +14 -0
- package/src/utils/ast-scope.ts +558 -0
- package/src/utils/ast-statement-order.ts +345 -0
- package/src/utils/email-date.ts +44 -0
- package/src/utils/html-content-model.ts +298 -0
- package/src/utils/storefront-tax.ts +128 -0
- package/src/utils/style-declarations.ts +117 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { generateCacheServerRuntime } from '../../src/cache/server-runtime'
|
|
2
|
+
import { loadEmittedModule } from '../_helpers/emitted-module-harness'
|
|
3
|
+
|
|
4
|
+
const CODE = generateCacheServerRuntime()
|
|
5
|
+
const SCOPE = 'ds_1:teleport_products'
|
|
6
|
+
|
|
7
|
+
interface VersionStoreStub {
|
|
8
|
+
isEnabled(): boolean
|
|
9
|
+
readVersions(): Promise<Record<string, number>>
|
|
10
|
+
bumpVersions(scopes: string[]): Promise<Record<string, number>>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const load = (store?: Partial<VersionStoreStub>) => {
|
|
14
|
+
const versionStore: VersionStoreStub = {
|
|
15
|
+
isEnabled: () => false,
|
|
16
|
+
readVersions: () => Promise.resolve({}),
|
|
17
|
+
bumpVersions: () => Promise.resolve({}),
|
|
18
|
+
...store,
|
|
19
|
+
}
|
|
20
|
+
return loadEmittedModule(CODE, { requireStub: () => versionStore })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A Next-style response recorder. */
|
|
24
|
+
const makeRes = (options: { withHeaders?: boolean } = {}) => {
|
|
25
|
+
const headers: Record<string, string> = {}
|
|
26
|
+
const recorded: { code?: number; body?: unknown } = {}
|
|
27
|
+
const res: Record<string, unknown> = {
|
|
28
|
+
status: (code: number) => {
|
|
29
|
+
recorded.code = code
|
|
30
|
+
return res
|
|
31
|
+
},
|
|
32
|
+
json: (body: unknown) => {
|
|
33
|
+
recorded.body = body
|
|
34
|
+
return res
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
if (options.withHeaders !== false) {
|
|
38
|
+
res.setHeader = (key: string, value: string) => {
|
|
39
|
+
headers[key] = value
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { res, headers, recorded }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const okHandler = (rows: unknown[]) =>
|
|
46
|
+
jest.fn(
|
|
47
|
+
async (_req: unknown, res: { status: (c: number) => { json: (b: unknown) => unknown } }) =>
|
|
48
|
+
res.status(200).json({ success: true, data: rows })
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
describe('emitted server cache runtime', () => {
|
|
52
|
+
it('serves the second identical request without calling the handler', async () => {
|
|
53
|
+
const runtime = load()
|
|
54
|
+
const handler = okHandler([{ id: 1 }])
|
|
55
|
+
const wrapped = runtime.tqWithCache(handler, { scope: SCOPE, ttl: 60 })
|
|
56
|
+
|
|
57
|
+
const first = makeRes()
|
|
58
|
+
await wrapped({ method: 'GET', query: { page: '1' } }, first.res)
|
|
59
|
+
const second = makeRes()
|
|
60
|
+
await wrapped({ method: 'GET', query: { page: '1' } }, second.res)
|
|
61
|
+
|
|
62
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
63
|
+
expect(first.headers['X-TQ-Cache']).toBe('MISS')
|
|
64
|
+
expect(second.headers['X-TQ-Cache']).toBe('HIT')
|
|
65
|
+
expect(second.recorded.body).toMatchObject({ success: true, data: [{ id: 1 }] })
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The count endpoint keeps its own KEYSPACE but borrows the data scope's
|
|
70
|
+
* VERSION row. Nothing ever bumps a ":count" row, so if the count versioned
|
|
71
|
+
* itself it could never be invalidated by a write — a create would refresh the
|
|
72
|
+
* grid while the pager kept insisting on the old total.
|
|
73
|
+
*/
|
|
74
|
+
it('stamps the count response with the DATA scope version', async () => {
|
|
75
|
+
const runtime = load({
|
|
76
|
+
isEnabled: () => true,
|
|
77
|
+
readVersions: () => Promise.resolve({ [SCOPE]: 7 }),
|
|
78
|
+
})
|
|
79
|
+
const counted = runtime.tqWithCache(okHandler([{ total: 24 }]), {
|
|
80
|
+
scope: `${SCOPE}:count`,
|
|
81
|
+
versionScope: SCOPE,
|
|
82
|
+
ttl: 600,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// A cold instance answers before its first version read resolves.
|
|
86
|
+
await counted({ method: 'GET', query: {} }, makeRes().res)
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
88
|
+
|
|
89
|
+
const after = makeRes()
|
|
90
|
+
await counted({ method: 'GET', query: {} }, after.res)
|
|
91
|
+
expect(after.headers['X-TQ-Version']).toBe('7')
|
|
92
|
+
expect(after.recorded.body).toMatchObject({ version: 7 })
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('would see NO version without versionScope, since nothing bumps a :count row', async () => {
|
|
96
|
+
const runtime = load({
|
|
97
|
+
isEnabled: () => true,
|
|
98
|
+
readVersions: () => Promise.resolve({ [SCOPE]: 7 }),
|
|
99
|
+
})
|
|
100
|
+
const counted = runtime.tqWithCache(okHandler([{ total: 24 }]), {
|
|
101
|
+
scope: `${SCOPE}:count`,
|
|
102
|
+
ttl: 600,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
await counted({ method: 'GET', query: {} }, makeRes().res)
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
107
|
+
|
|
108
|
+
const after = makeRes()
|
|
109
|
+
await counted({ method: 'GET', query: {} }, after.res)
|
|
110
|
+
expect(after.headers['X-TQ-Version']).toBeUndefined()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('purges a sub-scope when its parent scope is purged', async () => {
|
|
114
|
+
const runtime = load()
|
|
115
|
+
const dataHandler = okHandler([{ id: 1 }])
|
|
116
|
+
const countHandler = okHandler([{ total: 1 }])
|
|
117
|
+
const data = runtime.tqWithCache(dataHandler, { scope: SCOPE, ttl: 600 })
|
|
118
|
+
const count = runtime.tqWithCache(countHandler, {
|
|
119
|
+
scope: `${SCOPE}:count`,
|
|
120
|
+
versionScope: SCOPE,
|
|
121
|
+
ttl: 600,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
await data({ method: 'GET', query: {} }, makeRes().res)
|
|
125
|
+
await count({ method: 'GET', query: {} }, makeRes().res)
|
|
126
|
+
expect(dataHandler).toHaveBeenCalledTimes(1)
|
|
127
|
+
expect(countHandler).toHaveBeenCalledTimes(1)
|
|
128
|
+
|
|
129
|
+
// One purge of the products scope must take the total with it.
|
|
130
|
+
runtime.tqPurge(SCOPE)
|
|
131
|
+
|
|
132
|
+
await data({ method: 'GET', query: {} }, makeRes().res)
|
|
133
|
+
await count({ method: 'GET', query: {} }, makeRes().res)
|
|
134
|
+
expect(dataHandler).toHaveBeenCalledTimes(2)
|
|
135
|
+
expect(countHandler).toHaveBeenCalledTimes(2)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('never lets a scope purge reach a sibling table with a longer name', async () => {
|
|
139
|
+
const runtime = load()
|
|
140
|
+
const sibling = okHandler([{ id: 9 }])
|
|
141
|
+
const wrapped = runtime.tqWithCache(sibling, {
|
|
142
|
+
scope: `${SCOPE}_archive`,
|
|
143
|
+
ttl: 600,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
await wrapped({ method: 'GET', query: {} }, makeRes().res)
|
|
147
|
+
runtime.tqPurge(SCOPE)
|
|
148
|
+
await wrapped({ method: 'GET', query: {} }, makeRes().res)
|
|
149
|
+
|
|
150
|
+
expect(sibling).toHaveBeenCalledTimes(1)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('treats a different query as a different entry', async () => {
|
|
154
|
+
const runtime = load()
|
|
155
|
+
const handler = okHandler([{ id: 1 }])
|
|
156
|
+
const wrapped = runtime.tqWithCache(handler, { scope: SCOPE, ttl: 60 })
|
|
157
|
+
|
|
158
|
+
await wrapped({ method: 'GET', query: { page: '1' } }, makeRes().res)
|
|
159
|
+
await wrapped({ method: 'GET', query: { page: '2' } }, makeRes().res)
|
|
160
|
+
|
|
161
|
+
expect(handler).toHaveBeenCalledTimes(2)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('keys on the query independently of property order', async () => {
|
|
165
|
+
const runtime = load()
|
|
166
|
+
const handler = okHandler([{ id: 1 }])
|
|
167
|
+
const wrapped = runtime.tqWithCache(handler, { scope: SCOPE, ttl: 60 })
|
|
168
|
+
|
|
169
|
+
await wrapped({ method: 'GET', query: { page: '1', q: 'shoe' } }, makeRes().res)
|
|
170
|
+
await wrapped({ method: 'GET', query: { q: 'shoe', page: '1' } }, makeRes().res)
|
|
171
|
+
|
|
172
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('never caches a non-GET request', async () => {
|
|
176
|
+
const runtime = load()
|
|
177
|
+
const handler = okHandler([{ id: 1 }])
|
|
178
|
+
const wrapped = runtime.tqWithCache(handler, { scope: SCOPE, ttl: 60 })
|
|
179
|
+
|
|
180
|
+
await wrapped({ method: 'POST', query: {} }, makeRes().res)
|
|
181
|
+
await wrapped({ method: 'POST', query: {} }, makeRes().res)
|
|
182
|
+
|
|
183
|
+
expect(handler).toHaveBeenCalledTimes(2)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* A credentialed request is not a pure function of its URL, so it must never
|
|
188
|
+
* read from or write to an entry that another visitor could be served.
|
|
189
|
+
*/
|
|
190
|
+
it('bypasses the shared cache for a request carrying credentials', async () => {
|
|
191
|
+
const runtime = load()
|
|
192
|
+
const handler = okHandler([{ id: 1 }])
|
|
193
|
+
const wrapped = runtime.tqWithCache(handler, { scope: SCOPE, ttl: 60 })
|
|
194
|
+
|
|
195
|
+
await wrapped(
|
|
196
|
+
{ method: 'GET', query: { page: '1' }, headers: { authorization: 'Bearer x' } },
|
|
197
|
+
makeRes().res
|
|
198
|
+
)
|
|
199
|
+
await wrapped(
|
|
200
|
+
{ method: 'GET', query: { page: '1' }, headers: { authorization: 'Bearer x' } },
|
|
201
|
+
makeRes().res
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
expect(handler).toHaveBeenCalledTimes(2)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('never caches a failed response', async () => {
|
|
208
|
+
const runtime = load()
|
|
209
|
+
const handler = jest.fn(async (_req: unknown, res: ReturnType<typeof makeRes>['res']) =>
|
|
210
|
+
// tslint:disable-next-line:no-any
|
|
211
|
+
(res as any).status(500).json({ success: false, error: 'boom' })
|
|
212
|
+
)
|
|
213
|
+
const wrapped = runtime.tqWithCache(handler, { scope: SCOPE, ttl: 60 })
|
|
214
|
+
|
|
215
|
+
const first = makeRes()
|
|
216
|
+
await wrapped({ method: 'GET', query: {} }, first.res)
|
|
217
|
+
await wrapped({ method: 'GET', query: {} }, makeRes().res)
|
|
218
|
+
|
|
219
|
+
expect(handler).toHaveBeenCalledTimes(2)
|
|
220
|
+
expect(first.recorded.code).toBe(500)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* `fetchData`/`fetchCount` call the handler with a hand-rolled `res` that
|
|
225
|
+
* implements only `status` and `json`. A wrapper that assumed `setHeader`
|
|
226
|
+
* would throw on the getStaticProps prefetch of every page.
|
|
227
|
+
*/
|
|
228
|
+
it('survives a response object with no setHeader', async () => {
|
|
229
|
+
const runtime = load()
|
|
230
|
+
const wrapped = runtime.tqWithCache(okHandler([{ id: 1 }]), { scope: SCOPE, ttl: 60 })
|
|
231
|
+
const bare = makeRes({ withHeaders: false })
|
|
232
|
+
|
|
233
|
+
await expect(wrapped({ method: 'GET', query: {} }, bare.res)).resolves.not.toThrow()
|
|
234
|
+
expect(bare.recorded.body).toMatchObject({ success: true })
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('does not let a browser or CDN cache a private response', async () => {
|
|
238
|
+
const runtime = load()
|
|
239
|
+
const wrapped = runtime.tqWithCache(okHandler([]), { scope: SCOPE, ttl: 60 })
|
|
240
|
+
const res = makeRes()
|
|
241
|
+
await wrapped({ method: 'GET', query: {} }, res.res)
|
|
242
|
+
|
|
243
|
+
expect(res.headers['Cache-Control']).toBe('private, no-store')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('emits a shared CDN directive only when one was granted', async () => {
|
|
247
|
+
const runtime = load()
|
|
248
|
+
const wrapped = runtime.tqWithCache(okHandler([]), {
|
|
249
|
+
scope: SCOPE,
|
|
250
|
+
ttl: 60,
|
|
251
|
+
sMaxAge: 30,
|
|
252
|
+
swr: 300,
|
|
253
|
+
})
|
|
254
|
+
const res = makeRes()
|
|
255
|
+
await wrapped({ method: 'GET', query: {} }, res.res)
|
|
256
|
+
|
|
257
|
+
expect(res.headers['Cache-Control']).toBe('public, s-maxage=30, stale-while-revalidate=300')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The version read must never be awaited on the data path: a cold instance has
|
|
262
|
+
* an empty cache and needs no version, so blocking would add a database
|
|
263
|
+
* connect to the critical path to learn something that cannot change the
|
|
264
|
+
* answer.
|
|
265
|
+
*/
|
|
266
|
+
it('answers without waiting for the version lookup', async () => {
|
|
267
|
+
let settle: (value: Record<string, number>) => void = () => undefined
|
|
268
|
+
const pending = new Promise<Record<string, number>>((resolve) => {
|
|
269
|
+
settle = resolve
|
|
270
|
+
})
|
|
271
|
+
const runtime = load({ isEnabled: () => true, readVersions: () => pending })
|
|
272
|
+
const wrapped = runtime.tqWithCache(okHandler([{ id: 1 }]), { scope: SCOPE, ttl: 60 })
|
|
273
|
+
|
|
274
|
+
const res = makeRes()
|
|
275
|
+
await wrapped({ method: 'GET', query: {} }, res.res)
|
|
276
|
+
|
|
277
|
+
expect(res.recorded.code).toBe(200)
|
|
278
|
+
settle({})
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('drops its entries once the shared version moves', async () => {
|
|
282
|
+
let version = 1
|
|
283
|
+
const runtime = load({
|
|
284
|
+
isEnabled: () => true,
|
|
285
|
+
readVersions: () => Promise.resolve({ [SCOPE]: version }),
|
|
286
|
+
})
|
|
287
|
+
const handler = okHandler([{ id: 1 }])
|
|
288
|
+
const wrapped = runtime.tqWithCache(handler, { scope: SCOPE, ttl: 600 })
|
|
289
|
+
|
|
290
|
+
// Prime the memoised version, then fill the cache with it.
|
|
291
|
+
await wrapped({ method: 'GET', query: {} }, makeRes().res)
|
|
292
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
293
|
+
await wrapped({ method: 'GET', query: {} }, makeRes().res)
|
|
294
|
+
const callsBefore = handler.mock.calls.length
|
|
295
|
+
|
|
296
|
+
version = 2
|
|
297
|
+
const now = Date.now()
|
|
298
|
+
const spy = jest.spyOn(Date, 'now').mockReturnValue(now + 60_000)
|
|
299
|
+
await wrapped({ method: 'GET', query: {} }, makeRes().res)
|
|
300
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
301
|
+
await wrapped({ method: 'GET', query: {} }, makeRes().res)
|
|
302
|
+
spy.mockRestore()
|
|
303
|
+
|
|
304
|
+
expect(handler.mock.calls.length).toBeGreaterThan(callsBefore)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* A missing versions table must degrade to plain TTL caching, not break the
|
|
309
|
+
* data request — and it must stop asking, or every request pays for a failed
|
|
310
|
+
* database round trip.
|
|
311
|
+
*/
|
|
312
|
+
it('stops asking for versions after the table turns out to be missing', async () => {
|
|
313
|
+
const readVersions = jest.fn(() => {
|
|
314
|
+
const error = new Error('relation does not exist') as Error & { code: string }
|
|
315
|
+
error.code = '42P01'
|
|
316
|
+
return Promise.reject(error)
|
|
317
|
+
})
|
|
318
|
+
const runtime = load({ isEnabled: () => true, readVersions })
|
|
319
|
+
const wrapped = runtime.tqWithCache(okHandler([{ id: 1 }]), { scope: SCOPE, ttl: 60 })
|
|
320
|
+
|
|
321
|
+
await wrapped({ method: 'GET', query: { page: '1' } }, makeRes().res)
|
|
322
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
323
|
+
|
|
324
|
+
const now = Date.now()
|
|
325
|
+
const spy = jest.spyOn(Date, 'now').mockReturnValue(now + 60_000)
|
|
326
|
+
await wrapped({ method: 'GET', query: { page: '2' } }, makeRes().res)
|
|
327
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
328
|
+
spy.mockRestore()
|
|
329
|
+
|
|
330
|
+
expect(readVersions).toHaveBeenCalledTimes(1)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('bounds its memory with an LRU cap', async () => {
|
|
334
|
+
const runtime = load()
|
|
335
|
+
const handler = okHandler([{ id: 1 }])
|
|
336
|
+
const wrapped = runtime.tqWithCache(handler, { scope: SCOPE, ttl: 600 })
|
|
337
|
+
|
|
338
|
+
for (let i = 0; i < 210; i++) {
|
|
339
|
+
await wrapped({ method: 'GET', query: { page: String(i) } }, makeRes().res)
|
|
340
|
+
}
|
|
341
|
+
const callsAfterFill = handler.mock.calls.length
|
|
342
|
+
|
|
343
|
+
// The oldest entry has been evicted; the newest is still there.
|
|
344
|
+
await wrapped({ method: 'GET', query: { page: '0' } }, makeRes().res)
|
|
345
|
+
expect(handler.mock.calls.length).toBe(callsAfterFill + 1)
|
|
346
|
+
|
|
347
|
+
await wrapped({ method: 'GET', query: { page: '209' } }, makeRes().res)
|
|
348
|
+
expect(handler.mock.calls.length).toBe(callsAfterFill + 1)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('purges a single scope without touching the others', async () => {
|
|
352
|
+
const runtime = load()
|
|
353
|
+
const productsHandler = okHandler([{ id: 1 }])
|
|
354
|
+
const ordersHandler = okHandler([{ id: 2 }])
|
|
355
|
+
const products = runtime.tqWithCache(productsHandler, { scope: SCOPE, ttl: 600 })
|
|
356
|
+
const orders = runtime.tqWithCache(ordersHandler, { scope: 'ds_1:teleport_orders', ttl: 600 })
|
|
357
|
+
|
|
358
|
+
await products({ method: 'GET', query: {} }, makeRes().res)
|
|
359
|
+
await orders({ method: 'GET', query: {} }, makeRes().res)
|
|
360
|
+
|
|
361
|
+
runtime.tqPurge(SCOPE)
|
|
362
|
+
|
|
363
|
+
await products({ method: 'GET', query: {} }, makeRes().res)
|
|
364
|
+
await orders({ method: 'GET', query: {} }, makeRes().res)
|
|
365
|
+
|
|
366
|
+
expect(productsHandler).toHaveBeenCalledTimes(2)
|
|
367
|
+
expect(ordersHandler).toHaveBeenCalledTimes(1)
|
|
368
|
+
})
|
|
369
|
+
})
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { parse } from '@babel/parser'
|
|
2
|
+
import * as types from '@babel/types'
|
|
3
|
+
import { ASTScope } from '../../src'
|
|
4
|
+
|
|
5
|
+
const { collectBlockScopedBindings, collectReferencedIdentifiers } = ASTScope
|
|
6
|
+
|
|
7
|
+
const statementsOf = (code: string): types.Statement[] =>
|
|
8
|
+
parse(code, { sourceType: 'module', plugins: ['jsx'] }).program.body as types.Statement[]
|
|
9
|
+
|
|
10
|
+
const firstStatement = (code: string): types.Statement => statementsOf(code)[0]
|
|
11
|
+
|
|
12
|
+
const refs = (code: string, options?: ASTScope.ReferenceOptions): string[] =>
|
|
13
|
+
Array.from(collectReferencedIdentifiers(statementsOf(code), options)).sort()
|
|
14
|
+
|
|
15
|
+
describe('collectBlockScopedBindings', () => {
|
|
16
|
+
it('collects let/const/class bindings, including destructuring patterns', () => {
|
|
17
|
+
expect(collectBlockScopedBindings(firstStatement('const [a, setA] = useState(0)'))).toEqual([
|
|
18
|
+
'a',
|
|
19
|
+
'setA',
|
|
20
|
+
])
|
|
21
|
+
expect(
|
|
22
|
+
collectBlockScopedBindings(firstStatement('let { x, y: { z }, ...rest } = props'))
|
|
23
|
+
).toEqual(['x', 'z', 'rest'])
|
|
24
|
+
expect(collectBlockScopedBindings(firstStatement('class Foo {}'))).toEqual(['Foo'])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('excludes var and function declarations — they are hoisted, not TDZ-governed', () => {
|
|
28
|
+
expect(collectBlockScopedBindings(firstStatement('var a = 1'))).toEqual([])
|
|
29
|
+
expect(collectBlockScopedBindings(firstStatement('function foo() {}'))).toEqual([])
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('collectReferencedIdentifiers — what counts as a read', () => {
|
|
34
|
+
it('collects plain reads and ignores declared names', () => {
|
|
35
|
+
expect(refs('const a = b + c')).toEqual(['b', 'c'])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('ignores object keys and non-computed member properties', () => {
|
|
39
|
+
expect(refs('const p = { page: state.page, perPage: 20 }')).toEqual(['state'])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('collects computed member properties and computed keys', () => {
|
|
43
|
+
expect(refs('const v = obj[key]')).toEqual(['key', 'obj'])
|
|
44
|
+
expect(refs('const o = { [name]: 1 }')).toEqual(['name'])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('treats capitalized JSX tags as reads and intrinsic tags / attribute names as text', () => {
|
|
48
|
+
expect(refs('const el = <Repeater items={rows} />')).toEqual(['Repeater', 'rows'])
|
|
49
|
+
expect(refs('const el = <div className="x" />')).toEqual([])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('collects default values read by destructuring patterns', () => {
|
|
53
|
+
expect(refs('const { a = fallback } = props')).toEqual(['fallback', 'props'])
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('collectReferencedIdentifiers — immediate vs deferred', () => {
|
|
58
|
+
it('skips reads inside function bodies by default (they run later)', () => {
|
|
59
|
+
expect(refs('const handler = () => helper()')).toEqual([])
|
|
60
|
+
expect(refs('function run() { return helper() }')).toEqual([])
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('collects deferred reads when includeDeferred is on', () => {
|
|
64
|
+
expect(refs('const handler = () => helper()', { includeDeferred: true })).toEqual(['helper'])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('sees through useMemo: both the callback and the deps array run with the statement', () => {
|
|
68
|
+
expect(refs('const p = useMemo(() => ({ page: st.page }), [st])')).toEqual(['st', 'useMemo'])
|
|
69
|
+
// Even with an empty deps array, the callback itself is invoked during render.
|
|
70
|
+
expect(refs('const p = useMemo(() => st.page, [])')).toEqual(['st', 'useMemo'])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('sees through a useState lazy initializer', () => {
|
|
74
|
+
expect(refs('const [v, setV] = useState(() => seed.value)')).toEqual(['seed', 'useState'])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('keeps functions nested inside a sync callback deferred', () => {
|
|
78
|
+
expect(refs('const p = useMemo(() => () => later(), [])')).toEqual(['useMemo'])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('does NOT see into useEffect/useCallback callbacks — those never run during evaluation', () => {
|
|
82
|
+
expect(refs('useEffect(() => { mark(st) }, [])')).toEqual(['useEffect'])
|
|
83
|
+
expect(refs('const cb = useCallback(() => st.page, [])')).toEqual(['useCallback'])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('treats an IIFE body as immediate', () => {
|
|
87
|
+
expect(refs('const v = (function () { return seed.value })()')).toEqual(['seed'])
|
|
88
|
+
expect(refs('const v = (() => seed.value)()')).toEqual(['seed'])
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('collectReferencedIdentifiers — scoping', () => {
|
|
93
|
+
it('does not report names shadowed by function parameters', () => {
|
|
94
|
+
expect(refs('const f = (rows) => rows.length', { includeDeferred: true })).toEqual([])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('does not report names shadowed inside a nested block', () => {
|
|
98
|
+
expect(refs('{ const a = 1; use(a) }')).toEqual(['use'])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('does not report a catch binding', () => {
|
|
102
|
+
expect(refs('try { run() } catch (err) { log(err) }')).toEqual(['log', 'run'])
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('does not report for-of loop bindings', () => {
|
|
106
|
+
expect(refs('for (const row of rows) { push(row) }')).toEqual(['push', 'rows'])
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('shares one scope across switch cases', () => {
|
|
110
|
+
expect(refs('switch (kind) { case 1: const v = 1; break; default: use(v) }')).toEqual([
|
|
111
|
+
'kind',
|
|
112
|
+
'use',
|
|
113
|
+
])
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('honours the `only` filter', () => {
|
|
117
|
+
const only = new Set(['b'])
|
|
118
|
+
expect(refs('const a = b + c', { only })).toEqual(['b'])
|
|
119
|
+
})
|
|
120
|
+
})
|