@pikku/core 0.12.17 → 0.12.19
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 +20 -13
- package/dist/function/function-runner.js +22 -1
- package/dist/function/functions.types.d.ts +1 -0
- package/dist/services/in-memory-session-store.d.ts +8 -0
- package/dist/services/in-memory-session-store.js +12 -0
- package/dist/services/in-memory-workflow-service.d.ts +5 -2
- package/dist/services/in-memory-workflow-service.js +3 -1
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +1 -0
- package/dist/services/session-store.d.ts +6 -0
- package/dist/services/session-store.js +1 -0
- package/dist/services/user-session-service.d.ts +12 -10
- package/dist/services/user-session-service.js +18 -21
- package/dist/services/workflow-service.d.ts +5 -2
- package/dist/testing/service-tests.d.ts +2 -0
- package/dist/testing/service-tests.js +50 -11
- package/dist/types/core.types.d.ts +3 -0
- package/dist/wirings/channel/channel-store.d.ts +5 -6
- package/dist/wirings/channel/local/local-channel-runner.js +7 -4
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +25 -8
- package/dist/wirings/cli/channel/cli-channel-runner.js +4 -2
- package/dist/wirings/cli/cli-runner.js +5 -3
- package/dist/wirings/http/http-runner.js +1 -1
- package/dist/wirings/mcp/mcp-runner.js +1 -1
- package/dist/wirings/scheduler/scheduler-runner.js +1 -1
- package/dist/wirings/workflow/graph/graph-runner.js +4 -1
- package/dist/wirings/workflow/index.d.ts +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +5 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +6 -2
- package/dist/wirings/workflow/workflow.types.d.ts +14 -0
- package/package.json +1 -1
- package/src/function/function-runner.ts +36 -1
- package/src/function/functions.types.ts +5 -14
- package/src/services/in-memory-session-store.test.ts +58 -0
- package/src/services/in-memory-session-store.ts +21 -0
- package/src/services/in-memory-workflow-service.test.ts +21 -0
- package/src/services/in-memory-workflow-service.ts +8 -1
- package/src/services/index.ts +2 -0
- package/src/services/session-store.ts +9 -0
- package/src/services/user-session-service.test.ts +53 -19
- package/src/services/user-session-service.ts +21 -22
- package/src/services/workflow-service.ts +6 -1
- package/src/testing/service-tests.ts +64 -11
- package/src/types/core.types.ts +3 -0
- package/src/wirings/channel/channel-store.ts +5 -8
- package/src/wirings/channel/local/local-channel-runner.ts +10 -4
- package/src/wirings/channel/local/local-eventhub-service.test.ts +0 -5
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +30 -9
- package/src/wirings/cli/channel/cli-channel-runner.ts +4 -2
- package/src/wirings/cli/cli-runner.ts +7 -3
- package/src/wirings/http/http-runner.ts +3 -1
- package/src/wirings/mcp/mcp-runner.ts +3 -1
- package/src/wirings/scheduler/scheduler-runner.ts +1 -1
- package/src/wirings/workflow/graph/graph-runner.ts +5 -1
- package/src/wirings/workflow/index.ts +1 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +13 -3
- package/src/wirings/workflow/workflow.types.ts +15 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -34,7 +34,7 @@ export const runChannelConnect = async ({ channelId, channelObject, request, res
|
|
|
34
34
|
if (request instanceof Request) {
|
|
35
35
|
http = createHTTPWire(new PikkuFetchHTTPRequest(request), response);
|
|
36
36
|
}
|
|
37
|
-
const userSession = new PikkuSessionService(
|
|
37
|
+
const userSession = new PikkuSessionService(singletonServices.sessionStore);
|
|
38
38
|
const { channelConfig, openingData, meta } = await openChannel({
|
|
39
39
|
channelId,
|
|
40
40
|
request,
|
|
@@ -77,6 +77,11 @@ export const runChannelConnect = async ({ channelId, channelObject, request, res
|
|
|
77
77
|
channelMiddlewareMeta: meta.channelMiddleware,
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
+
// Store the pikkuUserId mapping after auth middleware has run
|
|
81
|
+
const pikkuUserId = userSession.getPikkuUserId();
|
|
82
|
+
if (pikkuUserId) {
|
|
83
|
+
await channelStore.setPikkuUserId(channelId, pikkuUserId);
|
|
84
|
+
}
|
|
80
85
|
http?.response?.status(101);
|
|
81
86
|
}
|
|
82
87
|
catch (e) {
|
|
@@ -98,21 +103,27 @@ export const runChannelDisconnect = async ({ channelId, channelStore, channelHan
|
|
|
98
103
|
// there's nothing to disconnect, so we can return early.
|
|
99
104
|
let channelData;
|
|
100
105
|
try {
|
|
101
|
-
channelData = await channelStore.
|
|
106
|
+
channelData = await channelStore.getChannel(channelId);
|
|
102
107
|
}
|
|
103
108
|
catch {
|
|
104
109
|
singletonServices.logger.info(`Channel ${channelId} not found during disconnect - already cleaned up`);
|
|
105
110
|
return;
|
|
106
111
|
}
|
|
107
|
-
const { openingData, channelName,
|
|
112
|
+
const { openingData, channelName, pikkuUserId } = channelData;
|
|
108
113
|
const { channel, channelConfig, meta } = getVariablesForChannel({
|
|
109
114
|
channelId,
|
|
110
115
|
channelHandlerFactory,
|
|
111
116
|
openingData,
|
|
112
117
|
channelName,
|
|
113
118
|
});
|
|
114
|
-
const userSession = new PikkuSessionService(
|
|
115
|
-
|
|
119
|
+
const userSession = new PikkuSessionService(singletonServices.sessionStore);
|
|
120
|
+
if (pikkuUserId) {
|
|
121
|
+
userSession.setPikkuUserId(pikkuUserId);
|
|
122
|
+
const session = await singletonServices.sessionStore?.get(pikkuUserId);
|
|
123
|
+
if (session) {
|
|
124
|
+
userSession.setInitial(session);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
116
127
|
const wire = {
|
|
117
128
|
channel,
|
|
118
129
|
...createMiddlewareSessionWireProps(userSession),
|
|
@@ -149,15 +160,21 @@ export const runChannelMessage = async ({ channelId, channelStore, channelHandle
|
|
|
149
160
|
const singletonServices = getSingletonServices();
|
|
150
161
|
const createWireServices = getCreateWireServices();
|
|
151
162
|
let wireServices;
|
|
152
|
-
const { openingData, channelName,
|
|
163
|
+
const { openingData, channelName, pikkuUserId } = await channelStore.getChannel(channelId);
|
|
153
164
|
const { channel, channelHandler, channelConfig } = getVariablesForChannel({
|
|
154
165
|
channelId,
|
|
155
166
|
channelHandlerFactory,
|
|
156
167
|
openingData,
|
|
157
168
|
channelName,
|
|
158
169
|
});
|
|
159
|
-
const userSession = new PikkuSessionService(
|
|
160
|
-
|
|
170
|
+
const userSession = new PikkuSessionService(singletonServices.sessionStore);
|
|
171
|
+
if (pikkuUserId) {
|
|
172
|
+
userSession.setPikkuUserId(pikkuUserId);
|
|
173
|
+
const session = await singletonServices.sessionStore?.get(pikkuUserId);
|
|
174
|
+
if (session) {
|
|
175
|
+
userSession.setInitial(session);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
161
178
|
const wire = {
|
|
162
179
|
channel,
|
|
163
180
|
...createMiddlewareSessionWireProps(userSession),
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { pikkuState } from '../../../pikku-state.js';
|
|
2
2
|
import { generateCommandHelp, parseCLIArguments } from '../command-parser.js';
|
|
3
3
|
/**
|
|
4
|
-
* Default JSON renderer for CLI output
|
|
4
|
+
* Default JSON renderer for CLI output. Emits single-line NDJSON so
|
|
5
|
+
* the output stays machine-parseable when the CLI is run with
|
|
6
|
+
* `--output json`; pretty-printing here would break NDJSON consumers.
|
|
5
7
|
*/
|
|
6
8
|
const defaultJSONRenderer = (_services, data) => {
|
|
7
|
-
console.log(JSON.stringify(data
|
|
9
|
+
console.log(JSON.stringify(data));
|
|
8
10
|
};
|
|
9
11
|
/**
|
|
10
12
|
* Execute a CLI program via WebSocket channel
|
|
@@ -17,10 +17,12 @@ export class CLIError extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* Default JSON renderer for CLI output
|
|
20
|
+
* Default JSON renderer for CLI output. Emits single-line NDJSON so
|
|
21
|
+
* the output stays machine-parseable when the CLI is run with
|
|
22
|
+
* `--output json`; pretty-printing here would break NDJSON consumers.
|
|
21
23
|
*/
|
|
22
24
|
const defaultJSONRenderer = (_services, data) => {
|
|
23
|
-
console.log(JSON.stringify(data
|
|
25
|
+
console.log(JSON.stringify(data));
|
|
24
26
|
};
|
|
25
27
|
/**
|
|
26
28
|
* Registers a CLI command tree and all its functions
|
|
@@ -224,7 +226,7 @@ export async function runCLICommand({ program, commandPath, data, singletonServi
|
|
|
224
226
|
},
|
|
225
227
|
state: 'open',
|
|
226
228
|
};
|
|
227
|
-
const userSession = new PikkuSessionService();
|
|
229
|
+
const userSession = new PikkuSessionService(singletonServices.sessionStore);
|
|
228
230
|
const wire = {
|
|
229
231
|
cli: {
|
|
230
232
|
program,
|
|
@@ -201,9 +201,9 @@ export const createHTTPWire = (request, response) => {
|
|
|
201
201
|
* @throws Throws errors like MissingSessionError or ForbiddenError on validation failures.
|
|
202
202
|
*/
|
|
203
203
|
const executeRoute = async (services, matchedRoute, http, options) => {
|
|
204
|
-
const userSession = new PikkuSessionService();
|
|
205
204
|
const { params, route, meta } = matchedRoute;
|
|
206
205
|
const { singletonServices, createWireServices, skipUserSession, requestId } = services;
|
|
206
|
+
const userSession = new PikkuSessionService(singletonServices.sessionStore);
|
|
207
207
|
// Attach URL parameters to the request object
|
|
208
208
|
http?.request?.setParams(params);
|
|
209
209
|
// Validate request headers if schema is defined
|
|
@@ -103,7 +103,7 @@ async function runMCPPikkuFunc(request, type, name, mcp, pikkuFuncId, { mcp: mcp
|
|
|
103
103
|
throw new NotFoundError(`MCP '${type}' PikkuFunction Mapping not found for '${name}'`);
|
|
104
104
|
}
|
|
105
105
|
singletonServices.logger.debug(`Running MCP ${type}: ${name}`);
|
|
106
|
-
const mcpSessionService = new PikkuSessionService();
|
|
106
|
+
const mcpSessionService = new PikkuSessionService(singletonServices.sessionStore);
|
|
107
107
|
const wire = {
|
|
108
108
|
mcp: mcpWire,
|
|
109
109
|
...createMiddlewareSessionWireProps(mcpSessionService),
|
|
@@ -41,7 +41,7 @@ export async function runScheduledTask({ name, session, traceId, }) {
|
|
|
41
41
|
singletonServices.logger;
|
|
42
42
|
const task = pikkuState(null, 'scheduler', 'tasks').get(name);
|
|
43
43
|
const meta = pikkuState(null, 'scheduler', 'meta')[name];
|
|
44
|
-
const userSession = new PikkuSessionService();
|
|
44
|
+
const userSession = new PikkuSessionService(singletonServices.sessionStore);
|
|
45
45
|
if (session) {
|
|
46
46
|
userSession.set(session);
|
|
47
47
|
}
|
|
@@ -580,7 +580,10 @@ export async function runWorkflowGraph(workflowService, graphName, triggerInput,
|
|
|
580
580
|
if (!meta.graphHash) {
|
|
581
581
|
throw new Error(`Workflow graph '${graphName}': missing graphHash in meta`);
|
|
582
582
|
}
|
|
583
|
-
const runId = await workflowService.createRun(graphName, triggerInput, inline ?? false, meta.graphHash, wire ?? { type: 'unknown' }
|
|
583
|
+
const runId = await workflowService.createRun(graphName, triggerInput, inline ?? false, meta.graphHash, wire ?? { type: 'unknown' }, {
|
|
584
|
+
deterministic: meta.deterministic,
|
|
585
|
+
plannedSteps: meta.plannedSteps,
|
|
586
|
+
});
|
|
584
587
|
if (inline) {
|
|
585
588
|
workflowService.registerInlineRun(runId);
|
|
586
589
|
}
|
|
@@ -8,5 +8,5 @@ export { pikkuWorkflowGraph, type PikkuWorkflowGraphConfig, type PikkuWorkflowGr
|
|
|
8
8
|
export { validateWorkflowWiring, computeEntryNodeIds, } from './graph/graph-validation.js';
|
|
9
9
|
export { pikkuWorkflowWorkerFunc, pikkuWorkflowOrchestratorFunc, pikkuWorkflowSleeperFunc, } from './workflow-queue-workers.js';
|
|
10
10
|
export type { WorkflowStepInput as WorkflowStepQueueInput, PikkuWorkflowOrchestratorInput, PikkuWorkflowSleeperInput, } from './workflow-queue-workers.js';
|
|
11
|
-
export type { WorkflowService, WorkflowServiceConfig, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, WorkflowRunStatus, StepState, WorkflowRunService, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowRuntimeMeta, WorkflowsRuntimeMeta, WorkflowStepInput, WorkflowOrchestratorInput, WorkflowSleeperInput, } from './workflow.types.js';
|
|
11
|
+
export type { WorkflowService, WorkflowServiceConfig, WorkflowPlannedStep, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, WorkflowRunStatus, StepState, WorkflowRunService, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowRuntimeMeta, WorkflowsRuntimeMeta, WorkflowStepInput, WorkflowOrchestratorInput, WorkflowSleeperInput, } from './workflow.types.js';
|
|
12
12
|
export type { WorkflowStepOptions, WorkflowWireDoRPC, WorkflowWireDoInline, WorkflowWireSleep, WorkflowWireSuspend, InputSource, OutputBinding, RpcStepMeta, SimpleCondition, Condition, BranchCase, BranchStepMeta, ParallelGroupStepMeta, FanoutStepMeta, ReturnStepMeta, InlineStepMeta, SleepStepMeta, CancelStepMeta, SetStepMeta, SwitchCaseMeta, SwitchStepMeta, FilterStepMeta, ArrayPredicateStepMeta, WorkflowStepMeta, WorkflowStepWire, PikkuWorkflowWire, } from './workflow.types.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SerializedError } from '../../types/core.types.js';
|
|
2
|
-
import type { PikkuWorkflowWire, StepState, WorkflowRun, WorkflowRunStatus, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, WorkflowStepOptions } from './workflow.types.js';
|
|
2
|
+
import type { PikkuWorkflowWire, StepState, WorkflowPlannedStep, WorkflowRun, WorkflowRunStatus, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, WorkflowStepOptions } from './workflow.types.js';
|
|
3
3
|
import type { WorkflowService } from '../../services/workflow-service.js';
|
|
4
4
|
import { PikkuError } from '../../errors/error-handler.js';
|
|
5
5
|
/**
|
|
@@ -62,7 +62,10 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
62
62
|
*/
|
|
63
63
|
unregisterInlineRun(runId: string): void;
|
|
64
64
|
registerWorkflowVersions(): Promise<void>;
|
|
65
|
-
abstract createRun(workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire
|
|
65
|
+
abstract createRun(workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire, options?: {
|
|
66
|
+
deterministic?: boolean;
|
|
67
|
+
plannedSteps?: WorkflowPlannedStep[];
|
|
68
|
+
}): Promise<string>;
|
|
66
69
|
/**
|
|
67
70
|
* Get a workflow run by ID
|
|
68
71
|
* @param id - Run ID
|
|
@@ -231,6 +231,8 @@ export class PikkuWorkflowService {
|
|
|
231
231
|
status: run.status,
|
|
232
232
|
startedAt: run.createdAt,
|
|
233
233
|
completedAt: terminalStatuses.has(run.status) ? run.updatedAt : undefined,
|
|
234
|
+
deterministic: run.deterministic,
|
|
235
|
+
plannedSteps: run.plannedSteps,
|
|
234
236
|
steps,
|
|
235
237
|
output: run.status === 'completed' ? run.output : undefined,
|
|
236
238
|
error: run.error
|
|
@@ -326,7 +328,10 @@ export class PikkuWorkflowService {
|
|
|
326
328
|
const shouldInline = options?.inline ||
|
|
327
329
|
workflowMeta.inline ||
|
|
328
330
|
!getSingletonServices()?.queueService;
|
|
329
|
-
const runId = await this.createRun(name, input, shouldInline, workflowMeta.graphHash, wire
|
|
331
|
+
const runId = await this.createRun(name, input, shouldInline, workflowMeta.graphHash, wire, {
|
|
332
|
+
deterministic: workflowMeta.deterministic,
|
|
333
|
+
plannedSteps: workflowMeta.plannedSteps,
|
|
334
|
+
});
|
|
330
335
|
if (shouldInline) {
|
|
331
336
|
this.inlineRuns.add(runId);
|
|
332
337
|
try {
|
|
@@ -425,7 +430,6 @@ export class PikkuWorkflowService {
|
|
|
425
430
|
const wire = {
|
|
426
431
|
workflow: workflowWire,
|
|
427
432
|
pikkuUserId: run.wire?.pikkuUserId,
|
|
428
|
-
session: rpcService.wire?.session,
|
|
429
433
|
rpc: rpcService.wire?.rpc,
|
|
430
434
|
};
|
|
431
435
|
try {
|
|
@@ -18,6 +18,10 @@ export interface WorkflowServiceConfig {
|
|
|
18
18
|
stepWorkerQueueName: string;
|
|
19
19
|
sleeperRPCName: string;
|
|
20
20
|
}
|
|
21
|
+
export interface WorkflowPlannedStep {
|
|
22
|
+
/** Human-readable step label for UI timeline */
|
|
23
|
+
stepName: string;
|
|
24
|
+
}
|
|
21
25
|
/**
|
|
22
26
|
* Workflow run status
|
|
23
27
|
*/
|
|
@@ -50,6 +54,10 @@ export interface WorkflowRun {
|
|
|
50
54
|
inline?: boolean;
|
|
51
55
|
/** Graph hash of the workflow definition at run creation time */
|
|
52
56
|
graphHash?: string;
|
|
57
|
+
/** True when the workflow has a static, pre-computable step timeline */
|
|
58
|
+
deterministic?: boolean;
|
|
59
|
+
/** Static planned steps snapshot captured at run start */
|
|
60
|
+
plannedSteps?: WorkflowPlannedStep[];
|
|
53
61
|
/** Wire origin info (how this run was started) */
|
|
54
62
|
wire: WorkflowRunWire;
|
|
55
63
|
/** Creation timestamp */
|
|
@@ -95,6 +103,8 @@ export interface WorkflowRunStatus {
|
|
|
95
103
|
status: WorkflowStatus;
|
|
96
104
|
startedAt: Date;
|
|
97
105
|
completedAt?: Date;
|
|
106
|
+
deterministic?: boolean;
|
|
107
|
+
plannedSteps?: WorkflowPlannedStep[];
|
|
98
108
|
steps: Array<{
|
|
99
109
|
name: string;
|
|
100
110
|
status: StepStatus;
|
|
@@ -211,6 +221,10 @@ export interface WorkflowRuntimeMeta {
|
|
|
211
221
|
entryNodeIds?: string[];
|
|
212
222
|
/** Hash of graph topology (nodes, edges, input mappings) */
|
|
213
223
|
graphHash?: string;
|
|
224
|
+
/** True when the workflow has a static, pre-computable step timeline */
|
|
225
|
+
deterministic?: boolean;
|
|
226
|
+
/** Static planned steps metadata for deterministic workflows */
|
|
227
|
+
plannedSteps?: WorkflowPlannedStep[];
|
|
214
228
|
}
|
|
215
229
|
/**
|
|
216
230
|
* Unified workflow runtime metadata map
|
package/package.json
CHANGED
|
@@ -26,15 +26,44 @@ import type {
|
|
|
26
26
|
} from './functions.types.js'
|
|
27
27
|
import { parseVersionedId } from '../version.js'
|
|
28
28
|
import type { SessionService } from '../services/user-session-service.js'
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
PikkuSessionService,
|
|
31
|
+
createFunctionSessionWireProps,
|
|
32
|
+
} from '../services/user-session-service.js'
|
|
30
33
|
import { ForbiddenError, ReadonlySessionError } from '../errors/errors.js'
|
|
31
34
|
import {
|
|
32
35
|
PikkuCredentialWireService,
|
|
33
36
|
createWireServicesCredentialWireProps,
|
|
34
37
|
} from '../services/credential-wire-service.js'
|
|
38
|
+
import { defaultPikkuUserIdResolver } from '../services/pikku-user-id.js'
|
|
35
39
|
import { rpcService } from '../wirings/rpc/rpc-runner.js'
|
|
36
40
|
import { closeWireServices } from '../utils.js'
|
|
37
41
|
|
|
42
|
+
async function resolveSession(
|
|
43
|
+
wire: PikkuWire,
|
|
44
|
+
singletonServices: CoreSingletonServices,
|
|
45
|
+
sessionService?: SessionService<CoreUserSession>
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
const pikkuUserId = defaultPikkuUserIdResolver(wire)
|
|
48
|
+
if (pikkuUserId) {
|
|
49
|
+
wire.pikkuUserId = pikkuUserId
|
|
50
|
+
if (sessionService instanceof PikkuSessionService) {
|
|
51
|
+
sessionService.setPikkuUserId(pikkuUserId)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { sessionStore } = singletonServices
|
|
56
|
+
if (!sessionStore || !pikkuUserId) return
|
|
57
|
+
|
|
58
|
+
if (!wire.session) {
|
|
59
|
+
const stored = await sessionStore.get(pikkuUserId)
|
|
60
|
+
if (stored) {
|
|
61
|
+
wire.session = stored
|
|
62
|
+
sessionService?.setInitial(stored)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
38
67
|
/**
|
|
39
68
|
* Get or create singleton services for an addon package.
|
|
40
69
|
* Services are cached in pikkuState to avoid recreation on each call.
|
|
@@ -256,6 +285,12 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
256
285
|
|
|
257
286
|
// Helper function to run permissions and execute the function
|
|
258
287
|
const executeFunction = async () => {
|
|
288
|
+
await resolveSession(
|
|
289
|
+
resolvedWire,
|
|
290
|
+
resolvedSingletonServices,
|
|
291
|
+
sessionService
|
|
292
|
+
)
|
|
293
|
+
|
|
259
294
|
if (sessionService) {
|
|
260
295
|
resolvedWire.session = sessionService.freezeInitial()
|
|
261
296
|
resolvedWire.setSession = (s: any) => sessionService.set(s)
|
|
@@ -74,15 +74,8 @@ export type CorePikkuFunctionSessionless<
|
|
|
74
74
|
export type CorePikkuPermission<
|
|
75
75
|
In = any,
|
|
76
76
|
Services extends CoreSingletonServices = CoreServices,
|
|
77
|
-
Wire extends PikkuWire<
|
|
78
|
-
In,
|
|
79
|
-
never,
|
|
80
|
-
false,
|
|
81
|
-
CoreUserSession,
|
|
82
|
-
any,
|
|
83
|
-
never,
|
|
84
|
-
never
|
|
85
|
-
> = PikkuWire<In, never, false, CoreUserSession, never, never, never>,
|
|
77
|
+
Wire extends PikkuWire<In, never, false, CoreUserSession, any, never, never> =
|
|
78
|
+
PikkuWire<In, never, false, CoreUserSession, never, never, never>,
|
|
86
79
|
> = (services: Services, data: In, wire: Wire) => Promise<boolean>
|
|
87
80
|
|
|
88
81
|
/**
|
|
@@ -269,11 +262,8 @@ export type CorePikkuFunctionConfig<
|
|
|
269
262
|
PikkuFunction extends
|
|
270
263
|
| CorePikkuFunction<any, any, any, any, any>
|
|
271
264
|
| CorePikkuFunctionSessionless<any, any, any, any, any>,
|
|
272
|
-
PikkuPermission extends CorePikkuPermission<
|
|
273
|
-
any
|
|
274
|
-
any,
|
|
275
|
-
any
|
|
276
|
-
> = CorePikkuPermission<any>,
|
|
265
|
+
PikkuPermission extends CorePikkuPermission<any, any, any> =
|
|
266
|
+
CorePikkuPermission<any>,
|
|
277
267
|
PikkuMiddleware extends CorePikkuMiddleware<any, any> = CorePikkuMiddleware<
|
|
278
268
|
any,
|
|
279
269
|
any
|
|
@@ -292,6 +282,7 @@ export type CorePikkuFunctionConfig<
|
|
|
292
282
|
remote?: boolean
|
|
293
283
|
mcp?: boolean
|
|
294
284
|
readonly?: boolean
|
|
285
|
+
deploy?: 'serverless' | 'server' | 'auto'
|
|
295
286
|
approvalRequired?: boolean
|
|
296
287
|
approvalDescription?: any
|
|
297
288
|
func: PikkuFunction
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { InMemorySessionStore } from './in-memory-session-store.js'
|
|
5
|
+
|
|
6
|
+
describe('InMemorySessionStore', () => {
|
|
7
|
+
test('get returns undefined for unknown user', async () => {
|
|
8
|
+
const store = new InMemorySessionStore()
|
|
9
|
+
const result = await store.get('unknown')
|
|
10
|
+
assert.equal(result, undefined)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('set and get round-trip', async () => {
|
|
14
|
+
const store = new InMemorySessionStore()
|
|
15
|
+
const session = { userId: 'user-1', organizationId: 'org-1' } as any
|
|
16
|
+
await store.set('user-1', session)
|
|
17
|
+
|
|
18
|
+
const result = await store.get('user-1')
|
|
19
|
+
assert.deepEqual(result, session)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('set overwrites previous session', async () => {
|
|
23
|
+
const store = new InMemorySessionStore()
|
|
24
|
+
await store.set('user-1', { userId: 'user-1', role: 'admin' } as any)
|
|
25
|
+
await store.set('user-1', { userId: 'user-1', role: 'member' } as any)
|
|
26
|
+
|
|
27
|
+
const result = await store.get('user-1')
|
|
28
|
+
assert.deepEqual(result, { userId: 'user-1', role: 'member' })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('clear removes session', async () => {
|
|
32
|
+
const store = new InMemorySessionStore()
|
|
33
|
+
await store.set('user-1', { userId: 'user-1' } as any)
|
|
34
|
+
assert.ok(await store.get('user-1'))
|
|
35
|
+
|
|
36
|
+
await store.clear('user-1')
|
|
37
|
+
const result = await store.get('user-1')
|
|
38
|
+
assert.equal(result, undefined)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('clear is no-op for unknown user', async () => {
|
|
42
|
+
const store = new InMemorySessionStore()
|
|
43
|
+
await store.clear('nonexistent')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('users are isolated', async () => {
|
|
47
|
+
const store = new InMemorySessionStore()
|
|
48
|
+
await store.set('user-a', { userId: 'a' } as any)
|
|
49
|
+
await store.set('user-b', { userId: 'b' } as any)
|
|
50
|
+
|
|
51
|
+
assert.deepEqual(await store.get('user-a'), { userId: 'a' })
|
|
52
|
+
assert.deepEqual(await store.get('user-b'), { userId: 'b' })
|
|
53
|
+
|
|
54
|
+
await store.clear('user-a')
|
|
55
|
+
assert.equal(await store.get('user-a'), undefined)
|
|
56
|
+
assert.deepEqual(await store.get('user-b'), { userId: 'b' })
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CoreUserSession } from '../types/core.types.js'
|
|
2
|
+
import type { SessionStore } from './session-store.js'
|
|
3
|
+
|
|
4
|
+
export class InMemorySessionStore<
|
|
5
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
6
|
+
> implements SessionStore<UserSession>
|
|
7
|
+
{
|
|
8
|
+
private sessions = new Map<string, UserSession>()
|
|
9
|
+
|
|
10
|
+
async get(pikkuUserId: string): Promise<UserSession | undefined> {
|
|
11
|
+
return this.sessions.get(pikkuUserId)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async set(pikkuUserId: string, session: UserSession): Promise<void> {
|
|
15
|
+
this.sessions.set(pikkuUserId, session)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async clear(pikkuUserId: string): Promise<void> {
|
|
19
|
+
this.sessions.delete(pikkuUserId)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -46,6 +46,27 @@ describe('InMemoryWorkflowService', () => {
|
|
|
46
46
|
const state = await service.getRunState(runId)
|
|
47
47
|
assert.deepStrictEqual(state, {})
|
|
48
48
|
})
|
|
49
|
+
|
|
50
|
+
test('should persist deterministic planned steps metadata', async () => {
|
|
51
|
+
const runId = await service.createRun(
|
|
52
|
+
'wf',
|
|
53
|
+
{},
|
|
54
|
+
true,
|
|
55
|
+
'h',
|
|
56
|
+
{} as any,
|
|
57
|
+
{
|
|
58
|
+
deterministic: true,
|
|
59
|
+
plannedSteps: [{ stepName: 'Step 1' }, { stepName: 'Step 2' }],
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const run = await service.getRun(runId)
|
|
64
|
+
assert.strictEqual(run?.deterministic, true)
|
|
65
|
+
assert.deepStrictEqual(run?.plannedSteps, [
|
|
66
|
+
{ stepName: 'Step 1' },
|
|
67
|
+
{ stepName: 'Step 2' },
|
|
68
|
+
])
|
|
69
|
+
})
|
|
49
70
|
})
|
|
50
71
|
|
|
51
72
|
describe('getRun', () => {
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
|
|
|
2
2
|
import { PikkuWorkflowService } from '../wirings/workflow/pikku-workflow-service.js'
|
|
3
3
|
import type { SerializedError } from '../types/core.types.js'
|
|
4
4
|
import type {
|
|
5
|
+
WorkflowPlannedStep,
|
|
5
6
|
WorkflowRun,
|
|
6
7
|
WorkflowRunService,
|
|
7
8
|
WorkflowRunWire,
|
|
@@ -55,7 +56,11 @@ export class InMemoryWorkflowService
|
|
|
55
56
|
input: any,
|
|
56
57
|
inline: boolean,
|
|
57
58
|
graphHash: string,
|
|
58
|
-
wire: WorkflowRunWire
|
|
59
|
+
wire: WorkflowRunWire,
|
|
60
|
+
options?: {
|
|
61
|
+
deterministic?: boolean
|
|
62
|
+
plannedSteps?: WorkflowPlannedStep[]
|
|
63
|
+
}
|
|
59
64
|
): Promise<string> {
|
|
60
65
|
const runId = randomUUID()
|
|
61
66
|
const now = new Date()
|
|
@@ -67,6 +72,8 @@ export class InMemoryWorkflowService
|
|
|
67
72
|
input,
|
|
68
73
|
inline,
|
|
69
74
|
graphHash,
|
|
75
|
+
deterministic: options?.deterministic,
|
|
76
|
+
plannedSteps: options?.plannedSteps,
|
|
70
77
|
wire,
|
|
71
78
|
createdAt: now,
|
|
72
79
|
updatedAt: now,
|
package/src/services/index.ts
CHANGED
|
@@ -68,6 +68,8 @@ export type {
|
|
|
68
68
|
} from './typed-credential-service.js'
|
|
69
69
|
export type { VariableStatus, VariableMeta } from './typed-variables-service.js'
|
|
70
70
|
export type { MetaService } from './meta-service.js'
|
|
71
|
+
export type { SessionStore } from './session-store.js'
|
|
72
|
+
export { InMemorySessionStore } from './in-memory-session-store.js'
|
|
71
73
|
export type {
|
|
72
74
|
MCPMeta,
|
|
73
75
|
RPCMetaRecord,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CoreUserSession } from '../types/core.types.js'
|
|
2
|
+
|
|
3
|
+
export interface SessionStore<
|
|
4
|
+
UserSession extends CoreUserSession = CoreUserSession,
|
|
5
|
+
> {
|
|
6
|
+
get(pikkuUserId: string): Promise<UserSession | undefined>
|
|
7
|
+
set(pikkuUserId: string, session: UserSession): Promise<void>
|
|
8
|
+
clear(pikkuUserId: string): Promise<void>
|
|
9
|
+
}
|
|
@@ -19,17 +19,17 @@ describe('PikkuSessionService', () => {
|
|
|
19
19
|
assert.strictEqual(service.get(), undefined)
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
-
test('should mark sessionChanged when set is called', () => {
|
|
22
|
+
test('should mark sessionChanged when set is called', async () => {
|
|
23
23
|
const service = new PikkuSessionService()
|
|
24
24
|
assert.strictEqual(service.sessionChanged, false)
|
|
25
|
-
service.set({ userId: 'user-1' })
|
|
25
|
+
await service.set({ userId: 'user-1' })
|
|
26
26
|
assert.strictEqual(service.sessionChanged, true)
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
-
test('should clear session', () => {
|
|
29
|
+
test('should clear session', async () => {
|
|
30
30
|
const service = new PikkuSessionService()
|
|
31
31
|
service.setInitial({ userId: 'user-1' })
|
|
32
|
-
service.clear()
|
|
32
|
+
await service.clear()
|
|
33
33
|
assert.strictEqual(service.get(), undefined)
|
|
34
34
|
assert.strictEqual(service.sessionChanged, true)
|
|
35
35
|
})
|
|
@@ -41,35 +41,69 @@ describe('PikkuSessionService', () => {
|
|
|
41
41
|
assert.deepStrictEqual(frozen, { userId: 'user-1' })
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
-
test('should only freeze initial once', () => {
|
|
44
|
+
test('should only freeze initial once', async () => {
|
|
45
45
|
const service = new PikkuSessionService()
|
|
46
46
|
service.setInitial({ userId: 'user-1' })
|
|
47
47
|
service.freezeInitial()
|
|
48
|
-
service.set({ userId: 'user-2' })
|
|
48
|
+
await service.set({ userId: 'user-2' })
|
|
49
49
|
const frozen2 = service.freezeInitial()
|
|
50
50
|
assert.deepStrictEqual(frozen2, { userId: 'user-1' })
|
|
51
51
|
})
|
|
52
52
|
|
|
53
|
-
test('should
|
|
54
|
-
|
|
55
|
-
message: 'Channel ID is required when using channel store',
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test('should use channelStore when provided', async () => {
|
|
53
|
+
test('should persist to sessionStore on set when pikkuUserId is set', async () => {
|
|
54
|
+
let storedId: string | undefined
|
|
60
55
|
let storedSession: any
|
|
61
56
|
const store = {
|
|
62
|
-
|
|
57
|
+
get: async () => undefined,
|
|
58
|
+
set: async (id: string, session: any) => {
|
|
59
|
+
storedId = id
|
|
63
60
|
storedSession = session
|
|
64
61
|
},
|
|
65
|
-
|
|
62
|
+
clear: async () => {},
|
|
66
63
|
}
|
|
67
|
-
const service = new PikkuSessionService(store as any
|
|
68
|
-
service.
|
|
64
|
+
const service = new PikkuSessionService(store as any)
|
|
65
|
+
service.setPikkuUserId('user-123')
|
|
66
|
+
await service.set({ userId: 'new' })
|
|
67
|
+
assert.strictEqual(storedId, 'user-123')
|
|
69
68
|
assert.deepStrictEqual(storedSession, { userId: 'new' })
|
|
69
|
+
})
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
test('should not persist to sessionStore when pikkuUserId is not set', async () => {
|
|
72
|
+
let setCalled = false
|
|
73
|
+
const store = {
|
|
74
|
+
get: async () => undefined,
|
|
75
|
+
set: async () => {
|
|
76
|
+
setCalled = true
|
|
77
|
+
},
|
|
78
|
+
clear: async () => {},
|
|
79
|
+
}
|
|
80
|
+
const service = new PikkuSessionService(store as any)
|
|
81
|
+
await service.set({ userId: 'new' })
|
|
82
|
+
assert.strictEqual(setCalled, false)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('should clear from sessionStore when pikkuUserId is set', async () => {
|
|
86
|
+
let clearedId: string | undefined
|
|
87
|
+
const store = {
|
|
88
|
+
get: async () => undefined,
|
|
89
|
+
set: async () => {},
|
|
90
|
+
clear: async (id: string) => {
|
|
91
|
+
clearedId = id
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
const service = new PikkuSessionService(store as any)
|
|
95
|
+
service.setPikkuUserId('user-123')
|
|
96
|
+
service.setInitial({ userId: 'user-123' })
|
|
97
|
+
await service.clear()
|
|
98
|
+
assert.strictEqual(clearedId, 'user-123')
|
|
99
|
+
assert.strictEqual(service.get(), undefined)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('getPikkuUserId should return set value', () => {
|
|
103
|
+
const service = new PikkuSessionService()
|
|
104
|
+
assert.strictEqual(service.getPikkuUserId(), undefined)
|
|
105
|
+
service.setPikkuUserId('user-456')
|
|
106
|
+
assert.strictEqual(service.getPikkuUserId(), 'user-456')
|
|
73
107
|
})
|
|
74
108
|
})
|
|
75
109
|
|