@pikku/core 0.12.20 → 0.12.21
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 -0
- package/dist/dev/hot-reload.js +20 -6
- package/dist/errors/error-handler.d.ts +1 -1
- package/dist/errors/error-handler.js +2 -2
- package/dist/function/functions.types.d.ts +3 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/middleware/index.d.ts +2 -2
- package/dist/middleware/index.js +2 -2
- package/dist/middleware/telemetry.d.ts +2 -2
- package/dist/middleware/telemetry.js +2 -2
- package/dist/middleware-runner.d.ts +15 -27
- package/dist/middleware-runner.js +25 -30
- package/dist/permissions.d.ts +15 -22
- package/dist/permissions.js +30 -25
- package/dist/pikku-request.js +1 -1
- package/dist/pikku-state.js +2 -3
- package/dist/services/ai-agent-runner-service.d.ts +1 -0
- package/dist/services/in-memory-queue-service.js +1 -1
- package/dist/services/in-memory-workflow-service.d.ts +15 -13
- package/dist/services/in-memory-workflow-service.js +31 -13
- package/dist/services/local-content.js +8 -1
- package/dist/services/user-session-service.d.ts +1 -1
- package/dist/testing/service-tests.js +24 -0
- package/dist/types/core.types.d.ts +4 -0
- package/dist/types/state.types.d.ts +2 -18
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +24 -5
- package/dist/wirings/ai-agent/ai-agent-memory.js +128 -23
- package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +10 -1
- package/dist/wirings/ai-agent/ai-agent-model-config.js +15 -35
- package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -1
- package/dist/wirings/ai-agent/ai-agent-runner.js +45 -31
- package/dist/wirings/ai-agent/ai-agent-stream.js +63 -34
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -0
- package/dist/wirings/channel/channel-handler.js +1 -1
- package/dist/wirings/channel/channel-store.d.ts +13 -0
- package/dist/wirings/channel/channel.types.d.ts +3 -0
- package/dist/wirings/channel/local/local-channel-runner.js +23 -5
- package/dist/wirings/channel/pikku-abstract-channel-handler.js +8 -0
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +9 -0
- package/dist/wirings/cli/cli-runner.js +24 -5
- package/dist/wirings/cli/command-parser.js +19 -4
- package/dist/wirings/http/http-runner.js +72 -36
- package/dist/wirings/http/http.types.d.ts +1 -0
- package/dist/wirings/http/pikku-fetch-http-request.js +3 -1
- package/dist/wirings/http/web-request.js +32 -0
- package/dist/wirings/rpc/rpc-runner.js +13 -3
- package/dist/wirings/workflow/graph/graph-node.d.ts +8 -0
- package/dist/wirings/workflow/graph/graph-node.js +4 -0
- package/dist/wirings/workflow/graph/graph-runner.js +12 -10
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +4 -0
- package/dist/wirings/workflow/index.d.ts +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +81 -16
- package/dist/wirings/workflow/pikku-workflow-service.js +266 -45
- package/dist/wirings/workflow/workflow.types.d.ts +34 -0
- package/package.json +1 -1
- package/run-tests.sh +4 -1
- package/src/dev/hot-reload.test.ts +62 -11
- package/src/dev/hot-reload.ts +28 -7
- package/src/errors/error-handler.ts +8 -2
- package/src/errors/error.test.ts +2 -0
- package/src/function/function-runner.test.ts +500 -10
- package/src/function/functions.types.ts +3 -2
- package/src/index.ts +12 -2
- package/src/middleware/index.ts +11 -2
- package/src/middleware/telemetry.ts +2 -2
- package/src/middleware-runner.test.ts +16 -16
- package/src/middleware-runner.ts +42 -30
- package/src/permissions.test.ts +27 -24
- package/src/permissions.ts +41 -25
- package/src/pikku-request.test.ts +35 -0
- package/src/pikku-request.ts +1 -1
- package/src/pikku-state.ts +2 -3
- package/src/run-tests-script.test.ts +18 -0
- package/src/services/ai-agent-runner-service.ts +1 -0
- package/src/services/in-memory-queue-service.ts +1 -1
- package/src/services/in-memory-session-store.ts +1 -2
- package/src/services/in-memory-workflow-service.test.ts +33 -11
- package/src/services/in-memory-workflow-service.ts +49 -13
- package/src/services/local-content.test.ts +54 -0
- package/src/services/local-content.ts +12 -1
- package/src/services/typed-credential-service.ts +3 -3
- package/src/services/typed-secret-service.ts +3 -3
- package/src/services/typed-variables-service.ts +3 -3
- package/src/services/user-session-service.ts +4 -4
- package/src/testing/service-tests.ts +30 -0
- package/src/types/core.types.ts +6 -2
- package/src/types/state.types.ts +2 -13
- package/src/wirings/ai-agent/ai-agent-memory.test.ts +324 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +187 -36
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +12 -90
- package/src/wirings/ai-agent/ai-agent-model-config.ts +14 -38
- package/src/wirings/ai-agent/ai-agent-prepare.test.ts +292 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -1
- package/src/wirings/ai-agent/ai-agent-registry.test.ts +230 -3
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +625 -6
- package/src/wirings/ai-agent/ai-agent-runner.ts +65 -50
- package/src/wirings/ai-agent/ai-agent-stream.test.ts +544 -5
- package/src/wirings/ai-agent/ai-agent-stream.ts +71 -69
- package/src/wirings/ai-agent/ai-agent.types.ts +24 -0
- package/src/wirings/channel/channel-handler.test.ts +272 -0
- package/src/wirings/channel/channel-handler.ts +1 -1
- package/src/wirings/channel/channel-middleware-runner.test.ts +163 -0
- package/src/wirings/channel/channel-store.ts +19 -0
- package/src/wirings/channel/channel.types.ts +4 -0
- package/src/wirings/channel/local/local-channel-runner.test.ts +63 -0
- package/src/wirings/channel/local/local-channel-runner.ts +41 -5
- package/src/wirings/channel/local/local-eventhub-service.ts +3 -3
- package/src/wirings/channel/pikku-abstract-channel-handler.ts +9 -2
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +23 -5
- package/src/wirings/cli/channel/cli-raw-channel-runner.ts +7 -2
- package/src/wirings/cli/cli-runner.test.ts +255 -2
- package/src/wirings/cli/cli-runner.ts +31 -10
- package/src/wirings/cli/cli.types.ts +4 -8
- package/src/wirings/cli/command-parser.test.ts +83 -0
- package/src/wirings/cli/command-parser.ts +23 -4
- package/src/wirings/http/http-runner.test.ts +296 -1
- package/src/wirings/http/http-runner.ts +87 -57
- package/src/wirings/http/http.types.ts +11 -26
- package/src/wirings/http/pikku-fetch-http-request.ts +8 -4
- package/src/wirings/http/pikku-fetch-http-response.test.ts +41 -0
- package/src/wirings/http/web-request.test.ts +115 -0
- package/src/wirings/http/web-request.ts +43 -5
- package/src/wirings/mcp/mcp-runner.test.ts +367 -0
- package/src/wirings/queue/queue.types.ts +2 -5
- package/src/wirings/rpc/rpc-runner.test.ts +511 -0
- package/src/wirings/rpc/rpc-runner.ts +15 -3
- package/src/wirings/rpc/wire-addon.test.ts +57 -0
- package/src/wirings/workflow/graph/graph-node.ts +12 -0
- package/src/wirings/workflow/graph/graph-runner.test.ts +98 -0
- package/src/wirings/workflow/graph/graph-runner.ts +28 -10
- package/src/wirings/workflow/graph/workflow-graph.types.ts +4 -0
- package/src/wirings/workflow/index.ts +1 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +19 -2
- package/src/wirings/workflow/pikku-workflow-service.ts +370 -71
- package/src/wirings/workflow/workflow-queue-workers.test.ts +131 -0
- package/src/wirings/workflow/workflow.types.ts +68 -5
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { beforeEach, describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { addFunction } from '../../function/function-runner.js'
|
|
5
|
+
import { pikkuState, resetPikkuState } from '../../pikku-state.js'
|
|
6
|
+
import {
|
|
7
|
+
ContextAwareRPCService,
|
|
8
|
+
RPCNotFoundError,
|
|
9
|
+
resolveNamespace,
|
|
10
|
+
rpcService,
|
|
11
|
+
} from './rpc-runner.js'
|
|
12
|
+
|
|
13
|
+
const createLogger = () => ({
|
|
14
|
+
debug: () => {},
|
|
15
|
+
info: () => {},
|
|
16
|
+
warn: () => {},
|
|
17
|
+
error: () => {},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const registerFunction = (
|
|
21
|
+
funcName: string,
|
|
22
|
+
func: (services: any, data: any, wire: any) => Promise<unknown> | unknown,
|
|
23
|
+
{
|
|
24
|
+
packageName = null,
|
|
25
|
+
expose,
|
|
26
|
+
pikkuFuncId,
|
|
27
|
+
tags,
|
|
28
|
+
}: {
|
|
29
|
+
packageName?: string | null
|
|
30
|
+
expose?: boolean
|
|
31
|
+
pikkuFuncId?: string
|
|
32
|
+
tags?: string[]
|
|
33
|
+
} = {}
|
|
34
|
+
) => {
|
|
35
|
+
addFunction(funcName, { func } as never, packageName)
|
|
36
|
+
pikkuState(packageName, 'function', 'meta')[funcName] = {
|
|
37
|
+
name: funcName,
|
|
38
|
+
pikkuFuncId,
|
|
39
|
+
sessionless: true,
|
|
40
|
+
permissions: [],
|
|
41
|
+
expose,
|
|
42
|
+
tags,
|
|
43
|
+
} as never
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const createServices = (overrides: Record<string, unknown> = {}) =>
|
|
47
|
+
({
|
|
48
|
+
logger: createLogger(),
|
|
49
|
+
...overrides,
|
|
50
|
+
}) as never
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
resetPikkuState()
|
|
54
|
+
pikkuState(null, 'package', 'singletonServices', createServices())
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('resolveNamespace', () => {
|
|
58
|
+
test('returns null for non-namespaced functions', () => {
|
|
59
|
+
assert.equal(resolveNamespace('plainFunction'), null)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('returns null when namespace is not registered', () => {
|
|
63
|
+
assert.equal(resolveNamespace('missing:func'), null)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('resolves addon namespace to package and function', () => {
|
|
67
|
+
pikkuState(null, 'addons', 'packages').set('stripe', {
|
|
68
|
+
package: '@addon/stripe',
|
|
69
|
+
auth: true,
|
|
70
|
+
tags: ['addon-tag'],
|
|
71
|
+
} as never)
|
|
72
|
+
|
|
73
|
+
assert.deepEqual(resolveNamespace('stripe:createCharge'), {
|
|
74
|
+
package: '@addon/stripe',
|
|
75
|
+
function: 'createCharge',
|
|
76
|
+
addonConfig: {
|
|
77
|
+
package: '@addon/stripe',
|
|
78
|
+
auth: true,
|
|
79
|
+
tags: ['addon-tag'],
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('ContextAwareRPCService.rpc', () => {
|
|
86
|
+
test('runs bare rpc functions through root rpc metadata', async () => {
|
|
87
|
+
pikkuState(null, 'rpc', 'meta').echo = 'echoFunc'
|
|
88
|
+
registerFunction('echoFunc', async (_services, data, wire) => ({
|
|
89
|
+
data,
|
|
90
|
+
traceId: wire.traceId,
|
|
91
|
+
}))
|
|
92
|
+
|
|
93
|
+
const service = new ContextAwareRPCService(
|
|
94
|
+
createServices(),
|
|
95
|
+
{ traceId: 'trace-1' } as never,
|
|
96
|
+
{ requiresAuth: false }
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const result = await service.rpc('echo', { hello: 'world' })
|
|
100
|
+
|
|
101
|
+
assert.deepEqual(result, {
|
|
102
|
+
data: { hello: 'world' },
|
|
103
|
+
traceId: 'trace-1',
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('uses package-scoped function meta before root rpc metadata', async () => {
|
|
108
|
+
pikkuState(null, 'rpc', 'meta').echo = 'rootEchoFunc'
|
|
109
|
+
registerFunction('rootEchoFunc', async () => ({ source: 'root' }))
|
|
110
|
+
registerFunction(
|
|
111
|
+
'packageEchoFunc',
|
|
112
|
+
async () => ({ source: '@addon/pkg' }),
|
|
113
|
+
{
|
|
114
|
+
packageName: '@addon/pkg',
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
pikkuState('@addon/pkg', 'function', 'meta').echo = {
|
|
118
|
+
name: 'echo',
|
|
119
|
+
pikkuFuncId: 'packageEchoFunc',
|
|
120
|
+
sessionless: true,
|
|
121
|
+
permissions: [],
|
|
122
|
+
} as never
|
|
123
|
+
|
|
124
|
+
const service = new ContextAwareRPCService(
|
|
125
|
+
createServices(),
|
|
126
|
+
{} as never,
|
|
127
|
+
{},
|
|
128
|
+
'@addon/pkg'
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
const result = await service.rpc('echo', {})
|
|
132
|
+
|
|
133
|
+
assert.deepEqual(result, { source: '@addon/pkg' })
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('falls back from versioned rpc names to the base metadata entry', async () => {
|
|
137
|
+
pikkuState(null, 'rpc', 'meta').echo = 'echoFunc'
|
|
138
|
+
registerFunction('echoFunc', async (_services, data) => data)
|
|
139
|
+
|
|
140
|
+
const service = new ContextAwareRPCService(
|
|
141
|
+
createServices(),
|
|
142
|
+
{} as never,
|
|
143
|
+
{}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
const result = await service.rpc('echo@v2', { ok: true })
|
|
147
|
+
|
|
148
|
+
assert.deepEqual(result, { ok: true })
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('resolves internal root functions that are registered outside rpc metadata', async () => {
|
|
152
|
+
registerFunction('pikkuWorkflowSleeper', async (_services, data) => ({
|
|
153
|
+
sleeper: true,
|
|
154
|
+
data,
|
|
155
|
+
}))
|
|
156
|
+
|
|
157
|
+
const service = new ContextAwareRPCService(
|
|
158
|
+
createServices(),
|
|
159
|
+
{} as never,
|
|
160
|
+
{}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
const result = await service.rpc('pikkuWorkflowSleeper', {
|
|
164
|
+
runId: 'run-1',
|
|
165
|
+
stepId: 'step-1',
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
assert.deepEqual(result, {
|
|
169
|
+
sleeper: true,
|
|
170
|
+
data: {
|
|
171
|
+
runId: 'run-1',
|
|
172
|
+
stepId: 'step-1',
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('invokes addon functions with addon auth and tags', async () => {
|
|
178
|
+
pikkuState(null, 'addons', 'packages').set('stripe', {
|
|
179
|
+
package: '@addon/stripe',
|
|
180
|
+
auth: false,
|
|
181
|
+
tags: ['addon-tag'],
|
|
182
|
+
} as never)
|
|
183
|
+
registerFunction(
|
|
184
|
+
'createCharge',
|
|
185
|
+
async (_services, data, wire) => ({
|
|
186
|
+
data,
|
|
187
|
+
auth: wire.wireType,
|
|
188
|
+
}),
|
|
189
|
+
{
|
|
190
|
+
packageName: '@addon/stripe',
|
|
191
|
+
tags: ['function-tag'],
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
pikkuState('@addon/stripe', 'function', 'meta').createCharge = {
|
|
196
|
+
name: 'createCharge',
|
|
197
|
+
sessionless: true,
|
|
198
|
+
permissions: [],
|
|
199
|
+
tags: ['function-tag'],
|
|
200
|
+
} as never
|
|
201
|
+
|
|
202
|
+
const service = new ContextAwareRPCService(
|
|
203
|
+
createServices({
|
|
204
|
+
logger: createLogger(),
|
|
205
|
+
}),
|
|
206
|
+
{ wireType: 'http', wireId: 'wire-1' } as never,
|
|
207
|
+
{ requiresAuth: true }
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
const seenTags: string[][] = []
|
|
211
|
+
const addonTagGroup = pikkuState('@addon/stripe', 'permissions', 'tagGroup')
|
|
212
|
+
addonTagGroup['addon-tag'] = [
|
|
213
|
+
async (_services: any, _data: any, wire: any) => {
|
|
214
|
+
seenTags.push(['addon-tag', wire.wireType])
|
|
215
|
+
return true
|
|
216
|
+
},
|
|
217
|
+
] as never
|
|
218
|
+
addonTagGroup['function-tag'] = [
|
|
219
|
+
async (_services: any, _data: any, wire: any) => {
|
|
220
|
+
seenTags.push(['function-tag', wire.wireType])
|
|
221
|
+
return true
|
|
222
|
+
},
|
|
223
|
+
] as never
|
|
224
|
+
|
|
225
|
+
const result = await service.rpc('stripe:createCharge', { amount: 10 })
|
|
226
|
+
|
|
227
|
+
assert.deepEqual(result, {
|
|
228
|
+
data: { amount: 10 },
|
|
229
|
+
auth: 'http',
|
|
230
|
+
})
|
|
231
|
+
assert.deepEqual(seenTags, [['addon-tag', 'http']])
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('falls back to deploymentService when rpc meta is missing', async () => {
|
|
235
|
+
const calls: unknown[][] = []
|
|
236
|
+
const service = new ContextAwareRPCService(
|
|
237
|
+
createServices({
|
|
238
|
+
deploymentService: {
|
|
239
|
+
invoke: async (...args: unknown[]) => {
|
|
240
|
+
calls.push(args)
|
|
241
|
+
return { remote: true }
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
}),
|
|
245
|
+
{
|
|
246
|
+
traceId: 'trace-2',
|
|
247
|
+
getSession: async () => ({ userId: 'user-1' }),
|
|
248
|
+
} as never,
|
|
249
|
+
{}
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
const result = await service.rpc('missingRpc', { value: 1 })
|
|
253
|
+
|
|
254
|
+
assert.deepEqual(result, { remote: true })
|
|
255
|
+
assert.deepEqual(calls, [
|
|
256
|
+
['missingRpc', { value: 1 }, { userId: 'user-1' }, 'trace-2'],
|
|
257
|
+
])
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test('throws RPCNotFoundError when rpc is missing and no deploymentService exists', async () => {
|
|
261
|
+
const service = new ContextAwareRPCService(
|
|
262
|
+
createServices(),
|
|
263
|
+
{} as never,
|
|
264
|
+
{}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
await assert.rejects(() => service.rpc('missingRpc', {}), RPCNotFoundError)
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
describe('ContextAwareRPCService.rpcExposed', () => {
|
|
272
|
+
test('resolves unversioned exposed rpc names through latest rpc metadata', async () => {
|
|
273
|
+
pikkuState(null, 'rpc', 'meta').listCards = 'listCards@v2'
|
|
274
|
+
registerFunction(
|
|
275
|
+
'listCards@v2',
|
|
276
|
+
async (_services, data) => ({ data, version: 2 }),
|
|
277
|
+
{
|
|
278
|
+
expose: true,
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
const service = new ContextAwareRPCService(
|
|
283
|
+
createServices(),
|
|
284
|
+
{} as never,
|
|
285
|
+
{}
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
const result = await service.rpcExposed('listCards', { ok: true })
|
|
289
|
+
|
|
290
|
+
assert.deepEqual(result, {
|
|
291
|
+
data: { ok: true },
|
|
292
|
+
version: 2,
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test('throws when the function does not exist', async () => {
|
|
297
|
+
const service = new ContextAwareRPCService(
|
|
298
|
+
createServices(),
|
|
299
|
+
{} as never,
|
|
300
|
+
{}
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
await assert.rejects(
|
|
304
|
+
() => service.rpcExposed('missingRpc', {}),
|
|
305
|
+
RPCNotFoundError
|
|
306
|
+
)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
test('throws RPCNotFoundError when the function is not exposed', async () => {
|
|
310
|
+
pikkuState(null, 'rpc', 'meta').hiddenFunc = 'hiddenFunc'
|
|
311
|
+
registerFunction('hiddenFunc', async () => ({ ok: true }), {
|
|
312
|
+
expose: false,
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
const service = new ContextAwareRPCService(
|
|
316
|
+
createServices(),
|
|
317
|
+
{} as never,
|
|
318
|
+
{}
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
await assert.rejects(
|
|
322
|
+
() => service.rpcExposed('hiddenFunc', {}),
|
|
323
|
+
RPCNotFoundError
|
|
324
|
+
)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test('runs exposed addon functions', async () => {
|
|
328
|
+
pikkuState(null, 'addons', 'packages').set('addon', {
|
|
329
|
+
package: '@addon/pkg',
|
|
330
|
+
} as never)
|
|
331
|
+
registerFunction(
|
|
332
|
+
'visibleFunc',
|
|
333
|
+
async (_services, data) => ({ data, source: 'addon' }),
|
|
334
|
+
{
|
|
335
|
+
packageName: '@addon/pkg',
|
|
336
|
+
expose: true,
|
|
337
|
+
}
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
const service = new ContextAwareRPCService(
|
|
341
|
+
createServices(),
|
|
342
|
+
{} as never,
|
|
343
|
+
{}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
const result = await service.rpcExposed('addon:visibleFunc', {
|
|
347
|
+
ok: true,
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
assert.deepEqual(result, { data: { ok: true }, source: 'addon' })
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
describe('ContextAwareRPCService.rpcWithWire', () => {
|
|
355
|
+
test('merges wire data for local rpc calls', async () => {
|
|
356
|
+
pikkuState(null, 'rpc', 'meta').echo = 'echoFunc'
|
|
357
|
+
registerFunction('echoFunc', async (_services, _data, wire) => ({
|
|
358
|
+
traceId: wire.traceId,
|
|
359
|
+
custom: wire.custom,
|
|
360
|
+
}))
|
|
361
|
+
|
|
362
|
+
const service = new ContextAwareRPCService(
|
|
363
|
+
createServices(),
|
|
364
|
+
{ traceId: 'base-trace' } as never,
|
|
365
|
+
{}
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
const result = await service.rpcWithWire('echo', {}, {
|
|
369
|
+
custom: 'merged',
|
|
370
|
+
} as never)
|
|
371
|
+
|
|
372
|
+
assert.deepEqual(result, {
|
|
373
|
+
traceId: 'base-trace',
|
|
374
|
+
custom: 'merged',
|
|
375
|
+
})
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
test('falls back to deploymentService for rpcWithWire when local rpc is missing', async () => {
|
|
379
|
+
const calls: unknown[][] = []
|
|
380
|
+
const service = new ContextAwareRPCService(
|
|
381
|
+
createServices({
|
|
382
|
+
deploymentService: {
|
|
383
|
+
invoke: async (...args: unknown[]) => {
|
|
384
|
+
calls.push(args)
|
|
385
|
+
return { remote: true }
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
}),
|
|
389
|
+
{ traceId: 'trace-3', session: { userId: 'user-2' } } as never,
|
|
390
|
+
{}
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
const result = await service.rpcWithWire('missingRpc', { value: 2 }, {
|
|
394
|
+
custom: 'wire',
|
|
395
|
+
} as never)
|
|
396
|
+
|
|
397
|
+
assert.deepEqual(result, { remote: true })
|
|
398
|
+
assert.deepEqual(calls, [
|
|
399
|
+
['missingRpc', { value: 2 }, { userId: 'user-2' }, 'trace-3'],
|
|
400
|
+
])
|
|
401
|
+
})
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
describe('ContextAwareRPCService.startWorkflow', () => {
|
|
405
|
+
test('throws when workflowService is unavailable', async () => {
|
|
406
|
+
const service = new ContextAwareRPCService(
|
|
407
|
+
createServices(),
|
|
408
|
+
{} as never,
|
|
409
|
+
{}
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
await assert.rejects(
|
|
413
|
+
() => service.startWorkflow('workflowA', {}),
|
|
414
|
+
/WorkflowService service not available/
|
|
415
|
+
)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
test('passes derived wire metadata into workflowService.startWorkflow', async () => {
|
|
419
|
+
const calls: unknown[][] = []
|
|
420
|
+
const service = new ContextAwareRPCService(
|
|
421
|
+
createServices({
|
|
422
|
+
workflowService: {
|
|
423
|
+
startWorkflow: async (...args: unknown[]) => {
|
|
424
|
+
calls.push(args)
|
|
425
|
+
return { runId: 'run-1' }
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
}),
|
|
429
|
+
{
|
|
430
|
+
wireType: 'rpc',
|
|
431
|
+
wireId: 'rpc-1',
|
|
432
|
+
pikkuUserId: 'user-3',
|
|
433
|
+
workflowStep: { runId: 'parent-run-1' },
|
|
434
|
+
} as never,
|
|
435
|
+
{}
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
const result = await service.startWorkflow('workflowA', { ok: true })
|
|
439
|
+
|
|
440
|
+
assert.deepEqual(result, { runId: 'run-1' })
|
|
441
|
+
assert.deepEqual(calls[0]?.slice(0, 3), [
|
|
442
|
+
'workflowA',
|
|
443
|
+
{ ok: true },
|
|
444
|
+
{
|
|
445
|
+
type: 'rpc',
|
|
446
|
+
id: 'rpc-1',
|
|
447
|
+
parentRunId: 'parent-run-1',
|
|
448
|
+
pikkuUserId: 'user-3',
|
|
449
|
+
},
|
|
450
|
+
])
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
describe('ContextAwareRPCService.remote', () => {
|
|
455
|
+
test('throws when deploymentService is unavailable', async () => {
|
|
456
|
+
const service = new ContextAwareRPCService(
|
|
457
|
+
createServices(),
|
|
458
|
+
{} as never,
|
|
459
|
+
{}
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
await assert.rejects(
|
|
463
|
+
() => service.remote('remoteFunc', {}),
|
|
464
|
+
/No DeploymentService configured for remote RPC: remoteFunc/
|
|
465
|
+
)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
test('uses deploymentService with session and trace id', async () => {
|
|
469
|
+
const calls: unknown[][] = []
|
|
470
|
+
const service = new ContextAwareRPCService(
|
|
471
|
+
createServices({
|
|
472
|
+
deploymentService: {
|
|
473
|
+
invoke: async (...args: unknown[]) => {
|
|
474
|
+
calls.push(args)
|
|
475
|
+
return { ok: true }
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
}),
|
|
479
|
+
{
|
|
480
|
+
traceId: 'trace-4',
|
|
481
|
+
getSession: async () => ({ userId: 'user-4' }),
|
|
482
|
+
} as never,
|
|
483
|
+
{}
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
const result = await service.remote('remoteFunc', { hi: true })
|
|
487
|
+
|
|
488
|
+
assert.deepEqual(result, { ok: true })
|
|
489
|
+
assert.deepEqual(calls, [
|
|
490
|
+
['remoteFunc', { hi: true }, { userId: 'user-4' }, 'trace-4'],
|
|
491
|
+
])
|
|
492
|
+
})
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
describe('rpcService.getContextRPCService', () => {
|
|
496
|
+
test('builds a typed rpc facade with depth and bound methods', async () => {
|
|
497
|
+
pikkuState(null, 'rpc', 'meta').echo = 'echoFunc'
|
|
498
|
+
registerFunction('echoFunc', async (_services, data) => data)
|
|
499
|
+
|
|
500
|
+
const rpc = rpcService.getContextRPCService(
|
|
501
|
+
createServices(),
|
|
502
|
+
{} as never,
|
|
503
|
+
false,
|
|
504
|
+
7
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
assert.equal(rpc.depth, 7)
|
|
508
|
+
assert.equal(rpc.global, false)
|
|
509
|
+
assert.deepEqual(await rpc.invoke('echo', { ok: true }), { ok: true })
|
|
510
|
+
})
|
|
511
|
+
})
|
|
@@ -3,7 +3,6 @@ import type { SessionService } from '../../services/user-session-service.js'
|
|
|
3
3
|
import type { CoreUserSession } from '../../types/core.types.js'
|
|
4
4
|
import { runPikkuFunc } from '../../function/function-runner.js'
|
|
5
5
|
import { pikkuState } from '../../pikku-state.js'
|
|
6
|
-
import { ForbiddenError } from '../../errors/errors.js'
|
|
7
6
|
import { PikkuError, addError } from '../../errors/error-handler.js'
|
|
8
7
|
import type { PikkuRPC, ResolvedFunction } from './rpc-types.js'
|
|
9
8
|
import { parseVersionedId } from '../../version.js'
|
|
@@ -81,6 +80,16 @@ const resolvePikkuFunction = (
|
|
|
81
80
|
rpcMeta = rpc[baseName]
|
|
82
81
|
}
|
|
83
82
|
}
|
|
83
|
+
if (!rpcMeta) {
|
|
84
|
+
const rootFunctions = pikkuState(null, 'function', 'meta')
|
|
85
|
+
const rootFunctionMeta = rootFunctions?.[rpcName]
|
|
86
|
+
if (rootFunctionMeta) {
|
|
87
|
+
return {
|
|
88
|
+
pikkuFuncId: rootFunctionMeta.pikkuFuncId || rpcName,
|
|
89
|
+
packageName: null,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
84
93
|
if (!rpcMeta) {
|
|
85
94
|
throw new RPCNotFoundError(rpcName)
|
|
86
95
|
}
|
|
@@ -109,13 +118,16 @@ export class ContextAwareRPCService {
|
|
|
109
118
|
]
|
|
110
119
|
}
|
|
111
120
|
} else {
|
|
112
|
-
|
|
121
|
+
const resolved = resolvePikkuFunction(funcName, this.packageName)
|
|
122
|
+
functionMeta = pikkuState(resolved.packageName, 'function', 'meta')[
|
|
123
|
+
resolved.pikkuFuncId
|
|
124
|
+
]
|
|
113
125
|
}
|
|
114
126
|
if (!functionMeta) {
|
|
115
127
|
throw new RPCNotFoundError(funcName)
|
|
116
128
|
}
|
|
117
129
|
if (!functionMeta.expose) {
|
|
118
|
-
throw new
|
|
130
|
+
throw new RPCNotFoundError(funcName)
|
|
119
131
|
}
|
|
120
132
|
return await this.rpc(funcName, data)
|
|
121
133
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { beforeEach, describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { resetPikkuState, pikkuState } from '../../pikku-state.js'
|
|
5
|
+
import { wireAddon } from './wire-addon.js'
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
resetPikkuState()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe('wireAddon', () => {
|
|
12
|
+
test('registers addon package metadata for namespace resolution', () => {
|
|
13
|
+
wireAddon({
|
|
14
|
+
name: 'stripe',
|
|
15
|
+
package: '@addon/stripe',
|
|
16
|
+
rpcEndpoint: 'https://rpc.example.com',
|
|
17
|
+
auth: true,
|
|
18
|
+
mcp: true,
|
|
19
|
+
tags: ['payments', 'billing'],
|
|
20
|
+
secretOverrides: { apiKey: 'secretName' },
|
|
21
|
+
variableOverrides: { region: 'eu-west-1' },
|
|
22
|
+
credentialOverrides: { oauth: 'credentialName' },
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
assert.deepEqual(pikkuState(null, 'addons', 'packages').get('stripe'), {
|
|
26
|
+
package: '@addon/stripe',
|
|
27
|
+
rpcEndpoint: 'https://rpc.example.com',
|
|
28
|
+
auth: true,
|
|
29
|
+
tags: ['payments', 'billing'],
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('overwrites existing addon config for the same namespace', () => {
|
|
34
|
+
wireAddon({
|
|
35
|
+
name: 'stripe',
|
|
36
|
+
package: '@addon/stripe-v1',
|
|
37
|
+
rpcEndpoint: 'https://rpc-v1.example.com',
|
|
38
|
+
auth: false,
|
|
39
|
+
tags: ['old'],
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
wireAddon({
|
|
43
|
+
name: 'stripe',
|
|
44
|
+
package: '@addon/stripe-v2',
|
|
45
|
+
rpcEndpoint: 'https://rpc-v2.example.com',
|
|
46
|
+
auth: true,
|
|
47
|
+
tags: ['new'],
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
assert.deepEqual(pikkuState(null, 'addons', 'packages').get('stripe'), {
|
|
51
|
+
package: '@addon/stripe-v2',
|
|
52
|
+
rpcEndpoint: 'https://rpc-v2.example.com',
|
|
53
|
+
auth: true,
|
|
54
|
+
tags: ['new'],
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -45,6 +45,10 @@ export interface GraphNodeDef<
|
|
|
45
45
|
next?: NextConfig<NodeIds>
|
|
46
46
|
/** Error handling - node(s) to execute on error */
|
|
47
47
|
onError?: NodeIds | NodeIds[]
|
|
48
|
+
/** Maximum retry attempts allowed (excludes the initial attempt) */
|
|
49
|
+
retries?: number
|
|
50
|
+
/** Delay between retries — milliseconds, duration string, or 'exponential' */
|
|
51
|
+
retryDelay?: string | number
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
/**
|
|
@@ -146,6 +150,8 @@ export function createGraph<RPCMap extends Record<string, RPCHandler>>() {
|
|
|
146
150
|
input: def?.input as any,
|
|
147
151
|
next: def?.next,
|
|
148
152
|
onError: def?.onError,
|
|
153
|
+
retries: def?.retries,
|
|
154
|
+
retryDelay: def?.retryDelay,
|
|
149
155
|
}
|
|
150
156
|
}
|
|
151
157
|
|
|
@@ -172,6 +178,8 @@ type GraphNodeConfigMap<
|
|
|
172
178
|
) => TypedRef<ComputeNodeOutputs<FuncMap, RPCMap>[N][P]>
|
|
173
179
|
) => InputWithRefs<ComputeNodeInputs<FuncMap, RPCMap>[K]>
|
|
174
180
|
onError?: Extract<keyof FuncMap, string> | Extract<keyof FuncMap, string>[]
|
|
181
|
+
retries?: number
|
|
182
|
+
retryDelay?: string | number
|
|
175
183
|
}
|
|
176
184
|
}
|
|
177
185
|
|
|
@@ -204,6 +212,8 @@ export function graph<NodeIds extends string = string>(
|
|
|
204
212
|
ref: (nodeId: NodeIds, path: string) => RefValue
|
|
205
213
|
) => Record<string, unknown>
|
|
206
214
|
onError?: NodeIds | NodeIds[]
|
|
215
|
+
retries?: number
|
|
216
|
+
retryDelay?: string | number
|
|
207
217
|
}
|
|
208
218
|
>
|
|
209
219
|
): Record<NodeIds, GraphNodeConfig<NodeIds>> {
|
|
@@ -215,6 +225,8 @@ export function graph<NodeIds extends string = string>(
|
|
|
215
225
|
input: def.input as any,
|
|
216
226
|
next: def.next,
|
|
217
227
|
onError: def.onError,
|
|
228
|
+
retries: def.retries,
|
|
229
|
+
retryDelay: def.retryDelay,
|
|
218
230
|
}
|
|
219
231
|
}
|
|
220
232
|
|