@pikku/core 0.12.4 → 0.12.5
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/CHANGELOG.md +11 -1
- package/dist/dev/hot-reload.d.ts +10 -0
- package/dist/dev/hot-reload.js +158 -0
- package/dist/middleware-runner.d.ts +1 -0
- package/dist/middleware-runner.js +5 -0
- package/dist/permissions.d.ts +1 -0
- package/dist/permissions.js +5 -0
- package/dist/services/in-memory-workflow-service.js +7 -11
- package/dist/wirings/ai-agent/ai-agent-prepare.js +16 -1
- package/dist/wirings/channel/channel-middleware-runner.d.ts +1 -0
- package/dist/wirings/channel/channel-middleware-runner.js +5 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +7 -8
- package/package.json +3 -2
- package/src/crypto-utils.test.ts +214 -0
- package/src/crypto-utils.ts +213 -0
- package/src/dev/hot-reload.test.ts +484 -0
- package/src/dev/hot-reload.ts +212 -0
- package/src/errors/error-handler.ts +62 -0
- package/src/errors/error.test.ts +195 -0
- package/src/errors/errors.ts +374 -0
- package/src/errors/index.ts +2 -0
- package/src/factory-functions.test.ts +109 -0
- package/src/function/function-runner.test.ts +536 -0
- package/src/function/function-runner.ts +365 -0
- package/src/function/functions.types.ts +304 -0
- package/src/function/index.ts +5 -0
- package/src/handle-error.test.ts +424 -0
- package/src/handle-error.ts +69 -0
- package/src/index.ts +118 -0
- package/src/internal.ts +6 -0
- package/src/middleware/auth-apikey.test.ts +363 -0
- package/src/middleware/auth-apikey.ts +47 -0
- package/src/middleware/auth-bearer.test.ts +450 -0
- package/src/middleware/auth-bearer.ts +72 -0
- package/src/middleware/auth-cookie.test.ts +528 -0
- package/src/middleware/auth-cookie.ts +80 -0
- package/src/middleware/cors.test.ts +424 -0
- package/src/middleware/cors.ts +104 -0
- package/src/middleware/index.ts +5 -0
- package/src/middleware/remote-auth.test.ts +488 -0
- package/src/middleware/remote-auth.ts +68 -0
- package/src/middleware/timeout.ts +15 -0
- package/src/middleware-runner.test.ts +418 -0
- package/src/middleware-runner.ts +240 -0
- package/src/permissions.test.ts +434 -0
- package/src/permissions.ts +327 -0
- package/src/pikku-request.ts +23 -0
- package/src/pikku-response.ts +5 -0
- package/src/pikku-state.test.ts +224 -0
- package/src/pikku-state.ts +216 -0
- package/src/run-tests-script.test.ts +49 -0
- package/src/schema.test.ts +249 -0
- package/src/schema.ts +151 -0
- package/src/services/ai-agent-runner-service.ts +41 -0
- package/src/services/ai-run-state-service.ts +20 -0
- package/src/services/ai-storage-service.ts +39 -0
- package/src/services/content-service.ts +76 -0
- package/src/services/deployment-service.ts +22 -0
- package/src/services/gateway-service.ts +20 -0
- package/src/services/gopass-secrets.ts +98 -0
- package/src/services/in-memory-ai-run-state-service.ts +73 -0
- package/src/services/in-memory-trigger-service.ts +64 -0
- package/src/services/in-memory-workflow-service.test.ts +351 -0
- package/src/services/in-memory-workflow-service.ts +502 -0
- package/src/services/index.ts +56 -0
- package/src/services/jwt-service.ts +30 -0
- package/src/services/local-content.ts +120 -0
- package/src/services/local-gateway-service.ts +62 -0
- package/src/services/local-secrets.test.ts +80 -0
- package/src/services/local-secrets.ts +94 -0
- package/src/services/local-variables.test.ts +61 -0
- package/src/services/local-variables.ts +39 -0
- package/src/services/logger-console.test.ts +118 -0
- package/src/services/logger-console.ts +98 -0
- package/src/services/logger.ts +57 -0
- package/src/services/scheduler-service.ts +86 -0
- package/src/services/schema-service.ts +33 -0
- package/src/services/scoped-secret-service.test.ts +74 -0
- package/src/services/scoped-secret-service.ts +41 -0
- package/src/services/secret-service.ts +38 -0
- package/src/services/trigger-service.ts +17 -0
- package/src/services/typed-secret-service.test.ts +93 -0
- package/src/services/typed-secret-service.ts +70 -0
- package/src/services/typed-variables-service.test.ts +73 -0
- package/src/services/typed-variables-service.ts +81 -0
- package/src/services/user-session-service.test.ts +113 -0
- package/src/services/user-session-service.ts +86 -0
- package/src/services/variables-service.ts +11 -0
- package/src/services/workflow-service.ts +95 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/service-tests.ts +873 -0
- package/src/time-utils.test.ts +56 -0
- package/src/time-utils.ts +107 -0
- package/src/types/core.types.ts +478 -0
- package/src/types/state.types.ts +188 -0
- package/src/utils/hash.test.ts +68 -0
- package/src/utils/hash.ts +26 -0
- package/src/utils.test.ts +350 -0
- package/src/utils.ts +129 -0
- package/src/version.test.ts +80 -0
- package/src/version.ts +25 -0
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +469 -0
- package/src/wirings/ai-agent/ai-agent-helpers.test.ts +152 -0
- package/src/wirings/ai-agent/ai-agent-helpers.ts +76 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +310 -0
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +115 -0
- package/src/wirings/ai-agent/ai-agent-model-config.ts +43 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +659 -0
- package/src/wirings/ai-agent/ai-agent-registry.test.ts +37 -0
- package/src/wirings/ai-agent/ai-agent-registry.ts +82 -0
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +319 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +773 -0
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +859 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +1153 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +382 -0
- package/src/wirings/ai-agent/index.ts +37 -0
- package/src/wirings/channel/channel-common.ts +90 -0
- package/src/wirings/channel/channel-handler.ts +250 -0
- package/src/wirings/channel/channel-middleware-runner.ts +126 -0
- package/src/wirings/channel/channel-runner.ts +166 -0
- package/src/wirings/channel/channel-store.ts +29 -0
- package/src/wirings/channel/channel.types.ts +172 -0
- package/src/wirings/channel/define-channel-routes.ts +25 -0
- package/src/wirings/channel/eventhub-service.ts +34 -0
- package/src/wirings/channel/eventhub-store.ts +13 -0
- package/src/wirings/channel/index.ts +24 -0
- package/src/wirings/channel/local/index.ts +3 -0
- package/src/wirings/channel/local/local-channel-handler.ts +81 -0
- package/src/wirings/channel/local/local-channel-runner.test.ts +215 -0
- package/src/wirings/channel/local/local-channel-runner.ts +210 -0
- package/src/wirings/channel/local/local-eventhub-service.test.ts +95 -0
- package/src/wirings/channel/local/local-eventhub-service.ts +101 -0
- package/src/wirings/channel/log-channels.ts +20 -0
- package/src/wirings/channel/pikku-abstract-channel-handler.test.ts +54 -0
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +45 -0
- package/src/wirings/channel/serverless/index.ts +5 -0
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +289 -0
- package/src/wirings/cli/channel/cli-channel-runner.ts +136 -0
- package/src/wirings/cli/channel/index.ts +1 -0
- package/src/wirings/cli/cli-runner.test.ts +403 -0
- package/src/wirings/cli/cli-runner.ts +529 -0
- package/src/wirings/cli/cli.types.ts +358 -0
- package/src/wirings/cli/command-parser.test.ts +445 -0
- package/src/wirings/cli/command-parser.ts +504 -0
- package/src/wirings/cli/define-cli-commands.ts +24 -0
- package/src/wirings/cli/index.ts +19 -0
- package/src/wirings/gateway/gateway-runner.test.ts +474 -0
- package/src/wirings/gateway/gateway-runner.ts +411 -0
- package/src/wirings/gateway/gateway.types.ts +149 -0
- package/src/wirings/gateway/index.ts +13 -0
- package/src/wirings/http/http-routes.test.ts +322 -0
- package/src/wirings/http/http-routes.ts +197 -0
- package/src/wirings/http/http-runner.test.ts +144 -0
- package/src/wirings/http/http-runner.ts +588 -0
- package/src/wirings/http/http.types.ts +369 -0
- package/src/wirings/http/index.ts +28 -0
- package/src/wirings/http/log-http-routes.ts +22 -0
- package/src/wirings/http/pikku-fetch-http-request.test.ts +237 -0
- package/src/wirings/http/pikku-fetch-http-request.ts +170 -0
- package/src/wirings/http/pikku-fetch-http-response.test.ts +82 -0
- package/src/wirings/http/pikku-fetch-http-response.ts +147 -0
- package/src/wirings/http/routers/http-router.ts +13 -0
- package/src/wirings/http/routers/path-to-regex.test.ts +319 -0
- package/src/wirings/http/routers/path-to-regex.ts +126 -0
- package/src/wirings/http/web-request.test.ts +236 -0
- package/src/wirings/http/web-request.ts +104 -0
- package/src/wirings/mcp/index.ts +27 -0
- package/src/wirings/mcp/mcp-endpoint-registry.test.ts +389 -0
- package/src/wirings/mcp/mcp-endpoint-registry.ts +159 -0
- package/src/wirings/mcp/mcp-runner.ts +323 -0
- package/src/wirings/mcp/mcp.types.ts +216 -0
- package/src/wirings/node/index.ts +2 -0
- package/src/wirings/node/node.types.ts +23 -0
- package/src/wirings/oauth2/index.ts +3 -0
- package/src/wirings/oauth2/oauth2-client.test.ts +929 -0
- package/src/wirings/oauth2/oauth2-client.ts +335 -0
- package/src/wirings/oauth2/oauth2.types.ts +69 -0
- package/src/wirings/oauth2/wire-oauth2-credential.ts +23 -0
- package/src/wirings/queue/index.ts +31 -0
- package/src/wirings/queue/queue-runner.test.ts +710 -0
- package/src/wirings/queue/queue-runner.ts +192 -0
- package/src/wirings/queue/queue.types.ts +189 -0
- package/src/wirings/queue/register-queue-helper.ts +60 -0
- package/src/wirings/queue/validate-worker-config.test.ts +108 -0
- package/src/wirings/queue/validate-worker-config.ts +116 -0
- package/src/wirings/rpc/index.ts +4 -0
- package/src/wirings/rpc/rpc-runner.ts +460 -0
- package/src/wirings/rpc/rpc-types.ts +56 -0
- package/src/wirings/rpc/wire-addon.ts +21 -0
- package/src/wirings/scheduler/index.ts +11 -0
- package/src/wirings/scheduler/log-schedulers.ts +20 -0
- package/src/wirings/scheduler/scheduler-runner.test.ts +660 -0
- package/src/wirings/scheduler/scheduler-runner.ts +133 -0
- package/src/wirings/scheduler/scheduler.types.ts +53 -0
- package/src/wirings/secret/index.ts +9 -0
- package/src/wirings/secret/secret.types.ts +32 -0
- package/src/wirings/secret/validate-secret-definitions.test.ts +140 -0
- package/src/wirings/secret/validate-secret-definitions.ts +82 -0
- package/src/wirings/trigger/index.ts +10 -0
- package/src/wirings/trigger/pikku-trigger-service.ts +112 -0
- package/src/wirings/trigger/trigger-runner.test.ts +79 -0
- package/src/wirings/trigger/trigger-runner.ts +135 -0
- package/src/wirings/trigger/trigger.types.ts +178 -0
- package/src/wirings/variable/index.ts +8 -0
- package/src/wirings/variable/validate-variable-definitions.test.ts +91 -0
- package/src/wirings/variable/validate-variable-definitions.ts +69 -0
- package/src/wirings/variable/variable.types.ts +22 -0
- package/src/wirings/workflow/dsl/index.ts +31 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +320 -0
- package/src/wirings/workflow/dsl/workflow-runner.ts +28 -0
- package/src/wirings/workflow/graph/graph-node.ts +222 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +487 -0
- package/src/wirings/workflow/graph/graph-runner.ts +816 -0
- package/src/wirings/workflow/graph/graph-validation.test.ts +170 -0
- package/src/wirings/workflow/graph/graph-validation.ts +237 -0
- package/src/wirings/workflow/graph/index.ts +19 -0
- package/src/wirings/workflow/graph/template.test.ts +49 -0
- package/src/wirings/workflow/graph/template.ts +42 -0
- package/src/wirings/workflow/graph/wire-workflow-graph.ts +29 -0
- package/src/wirings/workflow/graph/workflow-graph.types.ts +104 -0
- package/src/wirings/workflow/index.ts +82 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +200 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +1179 -0
- package/src/wirings/workflow/workflow-helpers.test.ts +129 -0
- package/src/wirings/workflow/workflow-helpers.ts +79 -0
- package/src/wirings/workflow/workflow.types.ts +274 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/lcov.info +0 -18891
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
const encoder = new TextEncoder()
|
|
2
|
+
const decoder = new TextDecoder()
|
|
3
|
+
|
|
4
|
+
const getSubtle = () => {
|
|
5
|
+
const crypto = globalThis.crypto
|
|
6
|
+
if (!crypto?.subtle) {
|
|
7
|
+
throw new Error('WebCrypto not available')
|
|
8
|
+
}
|
|
9
|
+
return crypto.subtle
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const base64Encode = (bytes: Uint8Array): string => {
|
|
13
|
+
if (typeof Buffer !== 'undefined') {
|
|
14
|
+
return Buffer.from(bytes).toString('base64')
|
|
15
|
+
}
|
|
16
|
+
let binary = ''
|
|
17
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
18
|
+
binary += String.fromCharCode(bytes[i]!)
|
|
19
|
+
}
|
|
20
|
+
return btoa(binary)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const base64Decode = (input: string): Uint8Array => {
|
|
24
|
+
if (typeof Buffer !== 'undefined') {
|
|
25
|
+
return new Uint8Array(Buffer.from(input, 'base64'))
|
|
26
|
+
}
|
|
27
|
+
const binary = atob(input)
|
|
28
|
+
const bytes = new Uint8Array(binary.length)
|
|
29
|
+
for (let i = 0; i < binary.length; i++) {
|
|
30
|
+
bytes[i] = binary.charCodeAt(i)
|
|
31
|
+
}
|
|
32
|
+
return bytes
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const toBase64Url = (bytes: Uint8Array): string => {
|
|
36
|
+
return base64Encode(bytes)
|
|
37
|
+
.replace(/\+/g, '-')
|
|
38
|
+
.replace(/\//g, '_')
|
|
39
|
+
.replace(/=+$/g, '')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const fromBase64Url = (input: string): Uint8Array => {
|
|
43
|
+
let base64 = input.replace(/-/g, '+').replace(/_/g, '/')
|
|
44
|
+
while (base64.length % 4 !== 0) base64 += '='
|
|
45
|
+
return base64Decode(base64)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const deriveKey = async (secret: string): Promise<CryptoKey> => {
|
|
49
|
+
const subtle = getSubtle()
|
|
50
|
+
const hash = await subtle.digest('SHA-256', encoder.encode(secret))
|
|
51
|
+
return subtle.importKey('raw', hash, { name: 'AES-GCM' }, false, [
|
|
52
|
+
'encrypt',
|
|
53
|
+
'decrypt',
|
|
54
|
+
])
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const encryptJSON = async (
|
|
58
|
+
secret: string,
|
|
59
|
+
value: unknown
|
|
60
|
+
): Promise<string> => {
|
|
61
|
+
const crypto = globalThis.crypto
|
|
62
|
+
if (!crypto?.getRandomValues) {
|
|
63
|
+
throw new Error('WebCrypto not available')
|
|
64
|
+
}
|
|
65
|
+
const subtle = getSubtle()
|
|
66
|
+
const iv = crypto.getRandomValues(new Uint8Array(12))
|
|
67
|
+
const key = await deriveKey(secret)
|
|
68
|
+
const plaintext = encoder.encode(JSON.stringify(value))
|
|
69
|
+
const encrypted = await subtle.encrypt(
|
|
70
|
+
{ name: 'AES-GCM', iv },
|
|
71
|
+
key,
|
|
72
|
+
plaintext
|
|
73
|
+
)
|
|
74
|
+
const cipherBytes = new Uint8Array(encrypted)
|
|
75
|
+
const out = new Uint8Array(iv.length + cipherBytes.length)
|
|
76
|
+
out.set(iv, 0)
|
|
77
|
+
out.set(cipherBytes, iv.length)
|
|
78
|
+
return toBase64Url(out)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const decryptJSON = async <T>(
|
|
82
|
+
secret: string,
|
|
83
|
+
token: string
|
|
84
|
+
): Promise<T> => {
|
|
85
|
+
const subtle = getSubtle()
|
|
86
|
+
const data = fromBase64Url(token)
|
|
87
|
+
if (data.length < 13) {
|
|
88
|
+
throw new Error('Invalid encrypted payload')
|
|
89
|
+
}
|
|
90
|
+
const iv = data.slice(0, 12)
|
|
91
|
+
const ciphertext = data.slice(12)
|
|
92
|
+
const key = await deriveKey(secret)
|
|
93
|
+
const decrypted = await subtle.decrypt(
|
|
94
|
+
{ name: 'AES-GCM', iv },
|
|
95
|
+
key,
|
|
96
|
+
ciphertext
|
|
97
|
+
)
|
|
98
|
+
return JSON.parse(decoder.decode(new Uint8Array(decrypted))) as T
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Envelope encryption utilities.
|
|
103
|
+
*
|
|
104
|
+
* Each secret gets its own random DEK (data encryption key).
|
|
105
|
+
* The DEK is wrapped (encrypted) by a KEK (key encryption key) — typically an env var.
|
|
106
|
+
* The actual secret is encrypted with the DEK.
|
|
107
|
+
*
|
|
108
|
+
* KEK rotation only re-wraps the DEK, never touches the ciphertext.
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
const importRawKey = async (rawBytes: Uint8Array): Promise<CryptoKey> => {
|
|
112
|
+
const subtle = getSubtle()
|
|
113
|
+
return subtle.importKey(
|
|
114
|
+
'raw',
|
|
115
|
+
rawBytes.buffer as ArrayBuffer,
|
|
116
|
+
{ name: 'AES-GCM' },
|
|
117
|
+
true,
|
|
118
|
+
['encrypt', 'decrypt']
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const generateDEK = async (): Promise<string> => {
|
|
123
|
+
const raw = globalThis.crypto.getRandomValues(new Uint8Array(32))
|
|
124
|
+
return toBase64Url(raw)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const wrapDEK = async (
|
|
128
|
+
kek: string,
|
|
129
|
+
plaintextDEK: string
|
|
130
|
+
): Promise<string> => {
|
|
131
|
+
return encryptJSON(kek, plaintextDEK)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const unwrapDEK = async (
|
|
135
|
+
kek: string,
|
|
136
|
+
wrappedDEK: string
|
|
137
|
+
): Promise<string> => {
|
|
138
|
+
return decryptJSON<string>(kek, wrappedDEK)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const encryptWithDEK = async (
|
|
142
|
+
dekBase64: string,
|
|
143
|
+
value: unknown
|
|
144
|
+
): Promise<string> => {
|
|
145
|
+
const crypto = globalThis.crypto
|
|
146
|
+
const subtle = getSubtle()
|
|
147
|
+
const iv = crypto.getRandomValues(new Uint8Array(12))
|
|
148
|
+
const key = await importRawKey(fromBase64Url(dekBase64))
|
|
149
|
+
const plaintext = encoder.encode(JSON.stringify(value))
|
|
150
|
+
const encrypted = await subtle.encrypt(
|
|
151
|
+
{ name: 'AES-GCM', iv },
|
|
152
|
+
key,
|
|
153
|
+
plaintext
|
|
154
|
+
)
|
|
155
|
+
const cipherBytes = new Uint8Array(encrypted)
|
|
156
|
+
const out = new Uint8Array(iv.length + cipherBytes.length)
|
|
157
|
+
out.set(iv, 0)
|
|
158
|
+
out.set(cipherBytes, iv.length)
|
|
159
|
+
return toBase64Url(out)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const decryptWithDEK = async <T>(
|
|
163
|
+
dekBase64: string,
|
|
164
|
+
token: string
|
|
165
|
+
): Promise<T> => {
|
|
166
|
+
const subtle = getSubtle()
|
|
167
|
+
const data = fromBase64Url(token)
|
|
168
|
+
if (data.length < 13) {
|
|
169
|
+
throw new Error('Invalid encrypted payload')
|
|
170
|
+
}
|
|
171
|
+
const iv = data.slice(0, 12)
|
|
172
|
+
const ciphertext = data.slice(12)
|
|
173
|
+
const key = await importRawKey(fromBase64Url(dekBase64))
|
|
174
|
+
const decrypted = await subtle.decrypt(
|
|
175
|
+
{ name: 'AES-GCM', iv },
|
|
176
|
+
key,
|
|
177
|
+
ciphertext
|
|
178
|
+
)
|
|
179
|
+
return JSON.parse(decoder.decode(new Uint8Array(decrypted))) as T
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface EnvelopeEncryptResult {
|
|
183
|
+
ciphertext: string
|
|
184
|
+
wrappedDEK: string
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const envelopeEncrypt = async (
|
|
188
|
+
kek: string,
|
|
189
|
+
value: unknown
|
|
190
|
+
): Promise<EnvelopeEncryptResult> => {
|
|
191
|
+
const dek = await generateDEK()
|
|
192
|
+
const ciphertext = await encryptWithDEK(dek, value)
|
|
193
|
+
const wrappedDEK = await wrapDEK(kek, dek)
|
|
194
|
+
return { ciphertext, wrappedDEK }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export const envelopeDecrypt = async <T>(
|
|
198
|
+
kek: string,
|
|
199
|
+
ciphertext: string,
|
|
200
|
+
wrappedDEK: string
|
|
201
|
+
): Promise<T> => {
|
|
202
|
+
const dek = await unwrapDEK(kek, wrappedDEK)
|
|
203
|
+
return decryptWithDEK<T>(dek, ciphertext)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export const envelopeRewrap = async (
|
|
207
|
+
oldKEK: string,
|
|
208
|
+
newKEK: string,
|
|
209
|
+
wrappedDEK: string
|
|
210
|
+
): Promise<string> => {
|
|
211
|
+
const dek = await unwrapDEK(oldKEK, wrappedDEK)
|
|
212
|
+
return wrapDEK(newKEK, dek)
|
|
213
|
+
}
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { describe, test, beforeEach, afterEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtemp, writeFile, rm, mkdir } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
|
|
7
|
+
import { pikkuState, resetPikkuState } from '../pikku-state.js'
|
|
8
|
+
import { addFunction } from '../function/function-runner.js'
|
|
9
|
+
import { fetch, wireHTTP } from '../wirings/http/http-runner.js'
|
|
10
|
+
import { httpRouter } from '../wirings/http/routers/http-router.js'
|
|
11
|
+
import {
|
|
12
|
+
wireScheduler,
|
|
13
|
+
runScheduledTask,
|
|
14
|
+
} from '../wirings/scheduler/scheduler-runner.js'
|
|
15
|
+
import { wireQueueWorker, runQueueJob } from '../wirings/queue/queue-runner.js'
|
|
16
|
+
import { pikkuDevReloader } from './hot-reload.js'
|
|
17
|
+
import {
|
|
18
|
+
PikkuMockRequest,
|
|
19
|
+
PikkuMockResponse,
|
|
20
|
+
} from '../wirings/channel/local/local-channel-runner.test.js'
|
|
21
|
+
|
|
22
|
+
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
23
|
+
|
|
24
|
+
const createMockLogger = () => {
|
|
25
|
+
const logs: Array<{ level: string; message: string }> = []
|
|
26
|
+
return {
|
|
27
|
+
info: (msg: string) => logs.push({ level: 'info', message: String(msg) }),
|
|
28
|
+
warn: (msg: string) => logs.push({ level: 'warn', message: String(msg) }),
|
|
29
|
+
error: (msg: string | Error) =>
|
|
30
|
+
logs.push({
|
|
31
|
+
level: 'error',
|
|
32
|
+
message: msg instanceof Error ? msg.message : String(msg),
|
|
33
|
+
}),
|
|
34
|
+
debug: (msg: string) => logs.push({ level: 'debug', message: String(msg) }),
|
|
35
|
+
getLogs: () => logs,
|
|
36
|
+
setLevel: () => {},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const writeFunctionModule = async (
|
|
41
|
+
dir: string,
|
|
42
|
+
filename: string,
|
|
43
|
+
returnValue: string
|
|
44
|
+
) => {
|
|
45
|
+
const jsContent = `export const ${filename.replace('.ts', '')} = { func: async () => (${returnValue}) };\n`
|
|
46
|
+
await writeFile(join(dir, filename.replace('.ts', '.js')), jsContent)
|
|
47
|
+
await writeFile(join(dir, filename), `// ts trigger ${Date.now()}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('pikkuDevReloader', () => {
|
|
51
|
+
let tmpDir: string
|
|
52
|
+
let reloader: { close: () => void } | undefined
|
|
53
|
+
let mockLogger: ReturnType<typeof createMockLogger>
|
|
54
|
+
|
|
55
|
+
beforeEach(async () => {
|
|
56
|
+
resetPikkuState()
|
|
57
|
+
httpRouter.reset()
|
|
58
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'pikku-hot-reload-test-'))
|
|
59
|
+
await writeFile(
|
|
60
|
+
join(tmpDir, 'package.json'),
|
|
61
|
+
JSON.stringify({ type: 'module' })
|
|
62
|
+
)
|
|
63
|
+
mockLogger = createMockLogger()
|
|
64
|
+
|
|
65
|
+
pikkuState(null, 'package', 'singletonServices', {
|
|
66
|
+
logger: mockLogger,
|
|
67
|
+
} as any)
|
|
68
|
+
pikkuState(null, 'package', 'factories', {
|
|
69
|
+
createWireServices: async () => ({}),
|
|
70
|
+
} as any)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
afterEach(async () => {
|
|
74
|
+
reloader?.close()
|
|
75
|
+
reloader = undefined
|
|
76
|
+
await rm(tmpDir, { recursive: true, force: true })
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('should hot-reload a function and pick up new return value', async () => {
|
|
80
|
+
addFunction('myFunc', {
|
|
81
|
+
func: async () => ({ version: 1 }),
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
await writeFunctionModule(tmpDir, 'myFunc.ts', '{ version: 1 }')
|
|
85
|
+
|
|
86
|
+
reloader = await pikkuDevReloader({
|
|
87
|
+
srcDirectories: [tmpDir],
|
|
88
|
+
logger: mockLogger,
|
|
89
|
+
pikkuDir: tmpDir,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const funcBefore = pikkuState(null, 'function', 'functions').get('myFunc')!
|
|
93
|
+
assert.deepEqual(await funcBefore.func({} as any, {}, {} as any), {
|
|
94
|
+
version: 1,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
await writeFunctionModule(tmpDir, 'myFunc.ts', '{ version: 2 }')
|
|
98
|
+
|
|
99
|
+
await wait(300)
|
|
100
|
+
|
|
101
|
+
const funcAfter = pikkuState(null, 'function', 'functions').get('myFunc')!
|
|
102
|
+
assert.deepEqual(await funcAfter.func({} as any, {}, {} as any), {
|
|
103
|
+
version: 2,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const reloadLog = mockLogger
|
|
107
|
+
.getLogs()
|
|
108
|
+
.find(
|
|
109
|
+
(l) =>
|
|
110
|
+
l.message.includes('Hot-reloaded') && l.message.includes('myFunc')
|
|
111
|
+
)
|
|
112
|
+
assert.ok(reloadLog, 'Should log hot-reload message')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('should not replace a function that is not registered', async () => {
|
|
116
|
+
addFunction('registeredFunc', {
|
|
117
|
+
func: async () => ({ name: 'registered' }),
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
await writeFunctionModule(tmpDir, 'unknownFunc.ts', '{ name: "unknown" }')
|
|
121
|
+
|
|
122
|
+
reloader = await pikkuDevReloader({
|
|
123
|
+
srcDirectories: [tmpDir],
|
|
124
|
+
logger: mockLogger,
|
|
125
|
+
pikkuDir: tmpDir,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
await writeFunctionModule(tmpDir, 'unknownFunc.ts', '{ name: "updated" }')
|
|
129
|
+
|
|
130
|
+
await wait(300)
|
|
131
|
+
|
|
132
|
+
assert.equal(
|
|
133
|
+
pikkuState(null, 'function', 'functions').has('unknownFunc'),
|
|
134
|
+
false
|
|
135
|
+
)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('should keep old code when JS import fails', async () => {
|
|
139
|
+
addFunction('badFunc', {
|
|
140
|
+
func: async () => ({ working: true }),
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
await writeFile(
|
|
144
|
+
join(tmpDir, 'badFunc.js'),
|
|
145
|
+
'export const badFunc = { func: async () => ({ working: true }) };\n'
|
|
146
|
+
)
|
|
147
|
+
await writeFile(join(tmpDir, 'badFunc.ts'), '// initial')
|
|
148
|
+
|
|
149
|
+
reloader = await pikkuDevReloader({
|
|
150
|
+
srcDirectories: [tmpDir],
|
|
151
|
+
logger: mockLogger,
|
|
152
|
+
pikkuDir: tmpDir,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
await writeFile(
|
|
156
|
+
join(tmpDir, 'badFunc.js'),
|
|
157
|
+
'this is not valid javascript {{{'
|
|
158
|
+
)
|
|
159
|
+
await writeFile(join(tmpDir, 'badFunc.ts'), `// trigger ${Date.now()}`)
|
|
160
|
+
|
|
161
|
+
await wait(300)
|
|
162
|
+
|
|
163
|
+
const func = pikkuState(null, 'function', 'functions').get('badFunc')!
|
|
164
|
+
assert.deepEqual(await func.func({} as any, {}, {} as any), {
|
|
165
|
+
working: true,
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test('should ignore non-ts files, test files, and gen files', async () => {
|
|
170
|
+
addFunction('someFunc', {
|
|
171
|
+
func: async () => ({ original: true }),
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
reloader = await pikkuDevReloader({
|
|
175
|
+
srcDirectories: [tmpDir],
|
|
176
|
+
logger: mockLogger,
|
|
177
|
+
pikkuDir: tmpDir,
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
await writeFile(join(tmpDir, 'someFunc.test.ts'), '// test file change')
|
|
181
|
+
await writeFile(join(tmpDir, 'someFunc.d.ts'), '// declaration file change')
|
|
182
|
+
await writeFile(join(tmpDir, 'someFunc.gen.ts'), '// gen file change')
|
|
183
|
+
await writeFile(join(tmpDir, 'readme.md'), '# changed')
|
|
184
|
+
|
|
185
|
+
await wait(300)
|
|
186
|
+
|
|
187
|
+
const reloadLogs = mockLogger
|
|
188
|
+
.getLogs()
|
|
189
|
+
.filter((l) => l.message.includes('Hot-reloaded'))
|
|
190
|
+
assert.equal(reloadLogs.length, 0)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('should hot-reload function used via HTTP wire', async () => {
|
|
194
|
+
const sessionMiddleware = async (_services: any, wire: any, next: any) => {
|
|
195
|
+
wire.setSession?.({ userId: 'test' } as any)
|
|
196
|
+
await next()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
pikkuState(null, 'function', 'meta', {
|
|
200
|
+
httpFunc: {
|
|
201
|
+
pikkuFuncId: 'httpFunc',
|
|
202
|
+
},
|
|
203
|
+
} as any)
|
|
204
|
+
pikkuState(null, 'http', 'meta', {
|
|
205
|
+
get: {
|
|
206
|
+
'/hot-test': {
|
|
207
|
+
pikkuFuncId: 'httpFunc',
|
|
208
|
+
route: '/hot-test',
|
|
209
|
+
method: 'get',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
post: {},
|
|
213
|
+
delete: {},
|
|
214
|
+
patch: {},
|
|
215
|
+
head: {},
|
|
216
|
+
put: {},
|
|
217
|
+
options: {},
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
addFunction('httpFunc', { func: async () => ({ value: 'old' }) })
|
|
221
|
+
|
|
222
|
+
wireHTTP({
|
|
223
|
+
route: '/hot-test',
|
|
224
|
+
method: 'get',
|
|
225
|
+
func: {
|
|
226
|
+
func: async () => ({ value: 'old' }),
|
|
227
|
+
middleware: [sessionMiddleware],
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
httpRouter.initialize()
|
|
231
|
+
|
|
232
|
+
const requestBefore = new PikkuMockRequest('/hot-test', 'get')
|
|
233
|
+
const responseBefore = await fetch(requestBefore)
|
|
234
|
+
assert.deepEqual(await responseBefore.json(), { value: 'old' })
|
|
235
|
+
|
|
236
|
+
await writeFunctionModule(tmpDir, 'httpFunc.ts', '{ value: "new" }')
|
|
237
|
+
|
|
238
|
+
reloader = await pikkuDevReloader({
|
|
239
|
+
srcDirectories: [tmpDir],
|
|
240
|
+
logger: mockLogger,
|
|
241
|
+
pikkuDir: tmpDir,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
await writeFunctionModule(tmpDir, 'httpFunc.ts', '{ value: "new" }')
|
|
245
|
+
|
|
246
|
+
await wait(300)
|
|
247
|
+
|
|
248
|
+
const funcAfter = pikkuState(null, 'function', 'functions').get('httpFunc')!
|
|
249
|
+
assert.deepEqual(await funcAfter.func({} as any, {}, {} as any), {
|
|
250
|
+
value: 'new',
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test('should hot-reload function used via scheduler wire', async () => {
|
|
255
|
+
const taskResult = { ref: 'initial' }
|
|
256
|
+
|
|
257
|
+
pikkuState(null, 'scheduler', 'meta')['hotTask'] = {
|
|
258
|
+
pikkuFuncId: 'hotTask',
|
|
259
|
+
name: 'hotTask',
|
|
260
|
+
schedule: '0 0 * * *',
|
|
261
|
+
}
|
|
262
|
+
pikkuState(null, 'function', 'meta')['hotTask'] = {
|
|
263
|
+
pikkuFuncId: 'hotTask',
|
|
264
|
+
inputSchemaName: null,
|
|
265
|
+
outputSchemaName: null,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
addFunction('hotTask', {
|
|
269
|
+
func: async () => {
|
|
270
|
+
taskResult.ref = 'v1'
|
|
271
|
+
},
|
|
272
|
+
auth: false,
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
wireScheduler({
|
|
276
|
+
name: 'hotTask',
|
|
277
|
+
schedule: '0 0 * * *',
|
|
278
|
+
func: {
|
|
279
|
+
func: async () => {
|
|
280
|
+
taskResult.ref = 'v1'
|
|
281
|
+
},
|
|
282
|
+
auth: false,
|
|
283
|
+
},
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
await runScheduledTask({ name: 'hotTask' })
|
|
287
|
+
assert.equal(taskResult.ref, 'v1')
|
|
288
|
+
|
|
289
|
+
const jsV1 = `export const hotTask = { func: async () => { }, auth: false };\n`
|
|
290
|
+
await writeFile(join(tmpDir, 'hotTask.js'), jsV1)
|
|
291
|
+
await writeFile(join(tmpDir, 'hotTask.ts'), '// initial')
|
|
292
|
+
|
|
293
|
+
reloader = await pikkuDevReloader({
|
|
294
|
+
srcDirectories: [tmpDir],
|
|
295
|
+
logger: mockLogger,
|
|
296
|
+
pikkuDir: tmpDir,
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
// Write updated function via file system and let the watcher pick it up
|
|
300
|
+
const jsV2 = `export const hotTask = { func: async () => { return { reloaded: true }; }, auth: false };\n`
|
|
301
|
+
await writeFile(join(tmpDir, 'hotTask.js'), jsV2)
|
|
302
|
+
await writeFile(join(tmpDir, 'hotTask.ts'), `// trigger ${Date.now()}`)
|
|
303
|
+
|
|
304
|
+
await wait(300)
|
|
305
|
+
|
|
306
|
+
// Verify the function was reloaded via the watcher
|
|
307
|
+
const reloadLog = mockLogger
|
|
308
|
+
.getLogs()
|
|
309
|
+
.find(
|
|
310
|
+
(l) =>
|
|
311
|
+
l.message.includes('Hot-reloaded') && l.message.includes('hotTask')
|
|
312
|
+
)
|
|
313
|
+
assert.ok(reloadLog, 'Should log hot-reload for hotTask')
|
|
314
|
+
|
|
315
|
+
// runScheduledTask uses runPikkuFunc which reads from the functions Map,
|
|
316
|
+
// so it will pick up the hot-reloaded function (no longer sets taskResult.ref)
|
|
317
|
+
await runScheduledTask({ name: 'hotTask' })
|
|
318
|
+
|
|
319
|
+
// The reloaded function no longer sets taskResult.ref, so it should
|
|
320
|
+
// still be 'v1' (proving the old function was replaced)
|
|
321
|
+
assert.equal(taskResult.ref, 'v1')
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test('should hot-reload function used via queue wire', async () => {
|
|
325
|
+
pikkuState(null, 'queue', 'meta')['hot-queue'] = {
|
|
326
|
+
pikkuFuncId: 'queue_hot-queue',
|
|
327
|
+
name: 'hot-queue',
|
|
328
|
+
}
|
|
329
|
+
pikkuState(null, 'function', 'meta')['queue_hot-queue'] = {
|
|
330
|
+
pikkuFuncId: 'queue_hot-queue',
|
|
331
|
+
inputSchemaName: null,
|
|
332
|
+
outputSchemaName: null,
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
addFunction('queue_hot-queue', {
|
|
336
|
+
func: async () => ({ result: 'v1' }),
|
|
337
|
+
auth: false,
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
wireQueueWorker({
|
|
341
|
+
name: 'hot-queue',
|
|
342
|
+
func: {
|
|
343
|
+
func: async () => ({ result: 'v1' }),
|
|
344
|
+
auth: false,
|
|
345
|
+
},
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const job = {
|
|
349
|
+
id: 'job-1',
|
|
350
|
+
queueName: 'hot-queue',
|
|
351
|
+
status: async () => 'active' as const,
|
|
352
|
+
data: {},
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const resultV1 = await runQueueJob({ job })
|
|
356
|
+
assert.deepEqual(resultV1, { result: 'v1' })
|
|
357
|
+
|
|
358
|
+
addFunction('queue_hot-queue', {
|
|
359
|
+
func: async () => ({ result: 'v2' }),
|
|
360
|
+
auth: false,
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
const job2 = {
|
|
364
|
+
id: 'job-2',
|
|
365
|
+
queueName: 'hot-queue',
|
|
366
|
+
status: async () => 'active' as const,
|
|
367
|
+
data: {},
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const resultV2 = await runQueueJob({ job: job2 })
|
|
371
|
+
assert.deepEqual(resultV2, { result: 'v2' })
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
test('should debounce rapid file changes', async () => {
|
|
375
|
+
addFunction('debounceFunc', {
|
|
376
|
+
func: async () => ({ count: 0 }),
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
await writeFunctionModule(tmpDir, 'debounceFunc.ts', '{ count: 0 }')
|
|
380
|
+
|
|
381
|
+
reloader = await pikkuDevReloader({
|
|
382
|
+
srcDirectories: [tmpDir],
|
|
383
|
+
logger: mockLogger,
|
|
384
|
+
pikkuDir: tmpDir,
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
for (let i = 1; i <= 5; i++) {
|
|
388
|
+
await writeFunctionModule(tmpDir, 'debounceFunc.ts', `{ count: ${i} }`)
|
|
389
|
+
await wait(10)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
await wait(300)
|
|
393
|
+
|
|
394
|
+
const func = pikkuState(null, 'function', 'functions').get('debounceFunc')!
|
|
395
|
+
const result = await func.func({} as any, {}, {} as any)
|
|
396
|
+
assert.equal(result.count, 5)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
test('should watch subdirectories', async () => {
|
|
400
|
+
const subDir = join(tmpDir, 'functions')
|
|
401
|
+
await mkdir(subDir)
|
|
402
|
+
|
|
403
|
+
addFunction('subFunc', {
|
|
404
|
+
func: async () => ({ nested: false }),
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
const jsContent = `export const subFunc = { func: async () => ({ nested: false }) };\n`
|
|
408
|
+
await writeFile(join(subDir, 'subFunc.js'), jsContent)
|
|
409
|
+
await writeFile(join(subDir, 'subFunc.ts'), '// initial')
|
|
410
|
+
|
|
411
|
+
reloader = await pikkuDevReloader({
|
|
412
|
+
srcDirectories: [tmpDir],
|
|
413
|
+
logger: mockLogger,
|
|
414
|
+
pikkuDir: tmpDir,
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const jsContentNew = `export const subFunc = { func: async () => ({ nested: true }) };\n`
|
|
418
|
+
await writeFile(join(subDir, 'subFunc.js'), jsContentNew)
|
|
419
|
+
await writeFile(join(subDir, 'subFunc.ts'), `// trigger ${Date.now()}`)
|
|
420
|
+
|
|
421
|
+
await wait(300)
|
|
422
|
+
|
|
423
|
+
const func = pikkuState(null, 'function', 'functions').get('subFunc')!
|
|
424
|
+
assert.deepEqual(await func.func({} as any, {}, {} as any), {
|
|
425
|
+
nested: true,
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
test('should properly clean up on close', async () => {
|
|
430
|
+
addFunction('cleanupFunc', {
|
|
431
|
+
func: async () => ({ v: 1 }),
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
await writeFunctionModule(tmpDir, 'cleanupFunc.ts', '{ v: 1 }')
|
|
435
|
+
|
|
436
|
+
reloader = await pikkuDevReloader({
|
|
437
|
+
srcDirectories: [tmpDir],
|
|
438
|
+
logger: mockLogger,
|
|
439
|
+
pikkuDir: tmpDir,
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
reloader.close()
|
|
443
|
+
|
|
444
|
+
await writeFunctionModule(tmpDir, 'cleanupFunc.ts', '{ v: 999 }')
|
|
445
|
+
|
|
446
|
+
await wait(300)
|
|
447
|
+
|
|
448
|
+
const func = pikkuState(null, 'function', 'functions').get('cleanupFunc')!
|
|
449
|
+
assert.deepEqual(await func.func({} as any, {}, {} as any), { v: 1 })
|
|
450
|
+
|
|
451
|
+
reloader = undefined
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
test('verifies in-flight request completes with old code after swap', async () => {
|
|
455
|
+
let resolveBlock: (() => void) | undefined
|
|
456
|
+
const blockPromise = new Promise<void>((resolve) => {
|
|
457
|
+
resolveBlock = resolve
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
addFunction('inflightFunc', {
|
|
461
|
+
func: async () => {
|
|
462
|
+
await blockPromise
|
|
463
|
+
return { version: 'old' }
|
|
464
|
+
},
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
const inflightPromise = pikkuState(null, 'function', 'functions')
|
|
468
|
+
.get('inflightFunc')!
|
|
469
|
+
.func({} as any, {}, {} as any)
|
|
470
|
+
|
|
471
|
+
addFunction('inflightFunc', {
|
|
472
|
+
func: async () => ({ version: 'new' }),
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
resolveBlock!()
|
|
476
|
+
const inflightResult = await inflightPromise
|
|
477
|
+
assert.deepEqual(inflightResult, { version: 'old' })
|
|
478
|
+
|
|
479
|
+
const newResult = await pikkuState(null, 'function', 'functions')
|
|
480
|
+
.get('inflightFunc')!
|
|
481
|
+
.func({} as any, {}, {} as any)
|
|
482
|
+
assert.deepEqual(newResult, { version: 'new' })
|
|
483
|
+
})
|
|
484
|
+
})
|