esoul-sdk 0.3.0 → 0.4.0
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/README.md +1 -1
- package/dist/helpers.d.ts +20 -0
- package/dist/helpers.js +29 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +17 -0
- package/dist/react.d.ts +29 -0
- package/dist/react.js +10 -0
- package/docs/05-ui.md +30 -0
- package/docs/07-background-tasks.md +6 -3
- package/llms-full.txt +37 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ that.
|
|
|
47
47
|
| Entry | What it gives you |
|
|
48
48
|
|---|---|
|
|
49
49
|
| `esoul-sdk` | `ApplicationSchema`, `EventDefinition`, `EventTypes`, `ApplicationIdentifier`, `incompleteStateNotice`, `deterministicReducerId`, `stableStringify`, `timingSafeEqual`, `nanoid`, `callPluginOp`, the manifest schema |
|
|
50
|
-
| `esoul-sdk/react` | `usePluginEventDispatch`, `useAppCanEdit`, `usePluginCurrentChatId`, `useWorkspaceTools`, file hooks |
|
|
50
|
+
| `esoul-sdk/react` | `usePluginEventDispatch`, `useAppCanEdit`, `usePluginCurrentChatId`, `useWorkspaceTools`, `usePluginRealtime`, file hooks |
|
|
51
51
|
| `esoul-sdk/server` | `PluginServerModule`, `readAppState`, `callWorkspaceTool`, `emitPluginAppEvent`, `getPluginConnectionCredentials`, file provider types |
|
|
52
52
|
| `esoul-sdk/testing` | a mock OAuth server for connection tests |
|
|
53
53
|
| `esoul-app validate <dir>` | validates a package folder against the manifest schema |
|
package/dist/helpers.d.ts
CHANGED
|
@@ -38,3 +38,23 @@ export declare function callPluginOp<T = unknown>(pluginId: string, op: string,
|
|
|
38
38
|
* Pure JS (no node:crypto) so it is safe in any runtime the SDK reaches.
|
|
39
39
|
*/
|
|
40
40
|
export declare function timingSafeEqual(a: string, b: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Kick one of your app's background tasks (docs/07) from the browser OR from
|
|
43
|
+
* a tool's `execute` on the server. The task must be listed in the
|
|
44
|
+
* manifest's `kickableTasks`; the platform's send-event route refuses the
|
|
45
|
+
* rest. `data` rides on `ctx.eventData` — keep it small and JSON. The kick
|
|
46
|
+
* is fire-and-forget: the task's result lands as events, or on the channel.
|
|
47
|
+
*/
|
|
48
|
+
export declare function kickPluginTask(args: {
|
|
49
|
+
applicationType: string;
|
|
50
|
+
taskName: string;
|
|
51
|
+
identifier: {
|
|
52
|
+
workspaceId: string;
|
|
53
|
+
nodeId: string;
|
|
54
|
+
instanceName?: string;
|
|
55
|
+
};
|
|
56
|
+
data?: Record<string, unknown>;
|
|
57
|
+
}): Promise<{
|
|
58
|
+
ok: boolean;
|
|
59
|
+
status: number;
|
|
60
|
+
}>;
|
package/dist/helpers.js
CHANGED
|
@@ -91,3 +91,32 @@ export function timingSafeEqual(a, b) {
|
|
|
91
91
|
}
|
|
92
92
|
return diff === 0;
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Kick one of your app's background tasks (docs/07) from the browser OR from
|
|
96
|
+
* a tool's `execute` on the server. The task must be listed in the
|
|
97
|
+
* manifest's `kickableTasks`; the platform's send-event route refuses the
|
|
98
|
+
* rest. `data` rides on `ctx.eventData` — keep it small and JSON. The kick
|
|
99
|
+
* is fire-and-forget: the task's result lands as events, or on the channel.
|
|
100
|
+
*/
|
|
101
|
+
export async function kickPluginTask(args) {
|
|
102
|
+
const onServer = typeof window === "undefined";
|
|
103
|
+
const base = onServer ? (process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000") : "";
|
|
104
|
+
const headers = { "Content-Type": "application/json" };
|
|
105
|
+
if (onServer && process.env.INTERNAL_TOOL_SECRET)
|
|
106
|
+
headers["x-esoul-internal-secret"] = process.env.INTERNAL_TOOL_SECRET;
|
|
107
|
+
const res = await fetch(`${base}/api/inngest/send-event`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers,
|
|
110
|
+
body: JSON.stringify({
|
|
111
|
+
name: `${args.applicationType}/${args.taskName}`,
|
|
112
|
+
data: {
|
|
113
|
+
workspaceId: args.identifier.workspaceId,
|
|
114
|
+
nodeId: args.identifier.nodeId,
|
|
115
|
+
applicationType: args.applicationType,
|
|
116
|
+
instanceName: args.identifier.instanceName,
|
|
117
|
+
...(args.data ?? {}),
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
return { ok: res.ok, status: res.status };
|
|
122
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,3 +4,22 @@ export * from "./helpers.js";
|
|
|
4
4
|
export * from "./files.js";
|
|
5
5
|
/** Id minting for dataCreators; an app depends on the SDK, not on nanoid. */
|
|
6
6
|
export { nanoid } from "nanoid";
|
|
7
|
+
/**
|
|
8
|
+
* Declare an app's realtime channel — one per instance. Inside the host this
|
|
9
|
+
* is the platform's real channel; outside it (tests, your editor) a
|
|
10
|
+
* descriptor of the same shape. Put it on the schema as `channel` and pass it
|
|
11
|
+
* to `usePluginRealtime`; a task publishes on it with `ctx.notify(topic, data)`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function definePluginChannel<T extends Record<string, {
|
|
14
|
+
schema: unknown;
|
|
15
|
+
}>>(args: {
|
|
16
|
+
applicationType: string;
|
|
17
|
+
topics: T;
|
|
18
|
+
}): ((p: {
|
|
19
|
+
workspaceId: string;
|
|
20
|
+
nodeId: string;
|
|
21
|
+
}) => Record<keyof T, unknown> & {
|
|
22
|
+
name: string;
|
|
23
|
+
}) & {
|
|
24
|
+
topicNames: (keyof T & string)[];
|
|
25
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -4,3 +4,20 @@ export * from "./helpers.js";
|
|
|
4
4
|
export * from "./files.js";
|
|
5
5
|
/** Id minting for dataCreators; an app depends on the SDK, not on nanoid. */
|
|
6
6
|
export { nanoid } from "nanoid";
|
|
7
|
+
/**
|
|
8
|
+
* Declare an app's realtime channel — one per instance. Inside the host this
|
|
9
|
+
* is the platform's real channel; outside it (tests, your editor) a
|
|
10
|
+
* descriptor of the same shape. Put it on the schema as `channel` and pass it
|
|
11
|
+
* to `usePluginRealtime`; a task publishes on it with `ctx.notify(topic, data)`.
|
|
12
|
+
*/
|
|
13
|
+
export function definePluginChannel(args) {
|
|
14
|
+
const type = args.applicationType.replace(/[^a-z0-9_]/gi, "_");
|
|
15
|
+
const factory = ((p) => {
|
|
16
|
+
const out = { name: `plugin:${type}:${p.workspaceId}:${p.nodeId}` };
|
|
17
|
+
for (const t of Object.keys(args.topics))
|
|
18
|
+
out[t] = { topic: t, channel: out.name };
|
|
19
|
+
return out;
|
|
20
|
+
});
|
|
21
|
+
factory.topicNames = Object.keys(args.topics);
|
|
22
|
+
return factory;
|
|
23
|
+
}
|
package/dist/react.d.ts
CHANGED
|
@@ -83,3 +83,32 @@ export declare function usePluginFileUpload(_workspaceId: string): (file: File)
|
|
|
83
83
|
export declare function useFileSources(_workspaceId: string | null): FileSourcesState;
|
|
84
84
|
/** One mount's listing; a failing source reports error/errorKind, never []. */
|
|
85
85
|
export declare function useFileSourceEntries(_workspaceId: string | null, _sourceId: string | null, _folderRef?: string): FileEntriesState;
|
|
86
|
+
export interface PluginRealtimeMessage<T = unknown> {
|
|
87
|
+
topic: string;
|
|
88
|
+
data: T;
|
|
89
|
+
}
|
|
90
|
+
export interface PluginRealtime<T = unknown> {
|
|
91
|
+
/** Every message received this mount, oldest first. */
|
|
92
|
+
data: PluginRealtimeMessage<T>[];
|
|
93
|
+
latestData: PluginRealtimeMessage<T> | null;
|
|
94
|
+
error: Error | null;
|
|
95
|
+
/** "connecting" | "active" | "closed" | "error". */
|
|
96
|
+
state: string;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Subscribe to this instance's channel (the one you declared with
|
|
100
|
+
* `definePluginChannel` and put on the schema as `channel`). A task publishes
|
|
101
|
+
* with `ctx.notify(topic, data)`; each message arrives here as
|
|
102
|
+
* `{ topic, data }`. Realtime is a NUDGE, never the truth: anything that must
|
|
103
|
+
* survive a refresh goes on the timeline as an event as well.
|
|
104
|
+
*/
|
|
105
|
+
export declare function usePluginRealtime<T = unknown>(_args: {
|
|
106
|
+
channel: (p: {
|
|
107
|
+
workspaceId: string;
|
|
108
|
+
nodeId: string;
|
|
109
|
+
}) => unknown;
|
|
110
|
+
workspaceId: string;
|
|
111
|
+
nodeId: string;
|
|
112
|
+
topics: readonly string[];
|
|
113
|
+
enabled?: boolean;
|
|
114
|
+
}): PluginRealtime<T>;
|
package/dist/react.js
CHANGED
|
@@ -41,3 +41,13 @@ export function useFileSources(_workspaceId) {
|
|
|
41
41
|
export function useFileSourceEntries(_workspaceId, _sourceId, _folderRef) {
|
|
42
42
|
return hostOnly("useFileSourceEntries");
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Subscribe to this instance's channel (the one you declared with
|
|
46
|
+
* `definePluginChannel` and put on the schema as `channel`). A task publishes
|
|
47
|
+
* with `ctx.notify(topic, data)`; each message arrives here as
|
|
48
|
+
* `{ topic, data }`. Realtime is a NUDGE, never the truth: anything that must
|
|
49
|
+
* survive a refresh goes on the timeline as an event as well.
|
|
50
|
+
*/
|
|
51
|
+
export function usePluginRealtime(_args) {
|
|
52
|
+
return hostOnly("usePluginRealtime");
|
|
53
|
+
}
|
package/docs/05-ui.md
CHANGED
|
@@ -63,6 +63,36 @@ The app renders inside the platform frame at any size: a desktop window, a maxim
|
|
|
63
63
|
`look_at_app` in the workbench screenshots desktop-light, desktop-dark and phone-light. Open the
|
|
64
64
|
images; a phone shot with a horizontal scrollbar is a bug.
|
|
65
65
|
|
|
66
|
+
## Live data from a background task
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { usePluginRealtime } from "esoul-sdk/react";
|
|
70
|
+
import { kickPluginTask } from "esoul-sdk";
|
|
71
|
+
import { stopwatchChannel } from "./channel";
|
|
72
|
+
|
|
73
|
+
const live = usePluginRealtime<{ runId: string; elapsedMs: number; serverNow: number }>({
|
|
74
|
+
channel: stopwatchChannel,
|
|
75
|
+
workspaceId: state.workspaceId,
|
|
76
|
+
nodeId: state.nodeId,
|
|
77
|
+
topics: stopwatchChannel.topicNames,
|
|
78
|
+
});
|
|
79
|
+
// live.latestData → { topic: "tick", data: {...} } — the newest message, or null.
|
|
80
|
+
// live.data → everything received this mount, oldest first.
|
|
81
|
+
|
|
82
|
+
const start = () =>
|
|
83
|
+
kickPluginTask({
|
|
84
|
+
applicationType: "plugin_stopwatch",
|
|
85
|
+
taskName: "tick",
|
|
86
|
+
identifier: state,
|
|
87
|
+
data: { runId, kickedAt: Date.now() },
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The subscription is per instance and read-only; the token the platform mints for it carries only
|
|
92
|
+
the topics your schema declares. Treat messages as nudges: what the UI must still show after a
|
|
93
|
+
refresh comes from state, so a task that changes anything durable dispatches an event as well
|
|
94
|
+
(docs/07 has the whole loop, including the stop).
|
|
95
|
+
|
|
66
96
|
## Cross-app from the UI
|
|
67
97
|
|
|
68
98
|
```ts
|
|
@@ -28,7 +28,8 @@ tasks: [
|
|
|
28
28
|
- `ctx.getState()` — the app's state, fresh.
|
|
29
29
|
- `ctx.dispatchEvent(eventName, eventData)` — through the full pipeline (append, fold, watermark),
|
|
30
30
|
using YOUR processors, so a task's mutation is identical to a tap's.
|
|
31
|
-
- `ctx.notify(topic, data)` —
|
|
31
|
+
- `ctx.notify(topic, data)` — publish on the app's realtime channel (see *Live tasks* below). A
|
|
32
|
+
nudge, never the truth: the UI hears it while mounted; a refresh only sees the timeline.
|
|
32
33
|
- `ctx.logger`.
|
|
33
34
|
|
|
34
35
|
## The replay model — read this twice
|
|
@@ -42,8 +43,10 @@ replay; mint them inside. Step names are unique per logical operation; loops inc
|
|
|
42
43
|
## How a task gets kicked
|
|
43
44
|
|
|
44
45
|
- **From a webhook**: `ctx.sendInngestEvent("<applicationType>/<task>", payload)` (docs/06).
|
|
45
|
-
- **From the browser**: list the task in `kickableTasks
|
|
46
|
-
|
|
46
|
+
- **From the browser or a tool**: list the task in `kickableTasks`, then
|
|
47
|
+
`kickPluginTask({ applicationType, taskName, identifier, data })` (from `esoul-sdk`; works in
|
|
48
|
+
a component and in a tool's `execute`). The platform's send-event route allows only listed
|
|
49
|
+
tasks and stamps nothing — `data` is exactly what `ctx.eventData` sees.
|
|
47
50
|
- **On a cadence**: `pollTasks: [{ task, everyMinutes }]` — one shared platform sweep kicks each
|
|
48
51
|
live instance at 5-minute granularity (minimum 5). This is your cron. There are deliberately
|
|
49
52
|
no per-app Inngest functions: function ids are fixed at module load, and the plan caps
|
package/llms-full.txt
CHANGED
|
@@ -51,7 +51,7 @@ that.
|
|
|
51
51
|
| Entry | What it gives you |
|
|
52
52
|
|---|---|
|
|
53
53
|
| `esoul-sdk` | `ApplicationSchema`, `EventDefinition`, `EventTypes`, `ApplicationIdentifier`, `incompleteStateNotice`, `deterministicReducerId`, `stableStringify`, `timingSafeEqual`, `nanoid`, `callPluginOp`, the manifest schema |
|
|
54
|
-
| `esoul-sdk/react` | `usePluginEventDispatch`, `useAppCanEdit`, `usePluginCurrentChatId`, `useWorkspaceTools`, file hooks |
|
|
54
|
+
| `esoul-sdk/react` | `usePluginEventDispatch`, `useAppCanEdit`, `usePluginCurrentChatId`, `useWorkspaceTools`, `usePluginRealtime`, file hooks |
|
|
55
55
|
| `esoul-sdk/server` | `PluginServerModule`, `readAppState`, `callWorkspaceTool`, `emitPluginAppEvent`, `getPluginConnectionCredentials`, file provider types |
|
|
56
56
|
| `esoul-sdk/testing` | a mock OAuth server for connection tests |
|
|
57
57
|
| `esoul-app validate <dir>` | validates a package folder against the manifest schema |
|
|
@@ -491,6 +491,36 @@ The app renders inside the platform frame at any size: a desktop window, a maxim
|
|
|
491
491
|
`look_at_app` in the workbench screenshots desktop-light, desktop-dark and phone-light. Open the
|
|
492
492
|
images; a phone shot with a horizontal scrollbar is a bug.
|
|
493
493
|
|
|
494
|
+
## Live data from a background task
|
|
495
|
+
|
|
496
|
+
```tsx
|
|
497
|
+
import { usePluginRealtime } from "esoul-sdk/react";
|
|
498
|
+
import { kickPluginTask } from "esoul-sdk";
|
|
499
|
+
import { stopwatchChannel } from "./channel";
|
|
500
|
+
|
|
501
|
+
const live = usePluginRealtime<{ runId: string; elapsedMs: number; serverNow: number }>({
|
|
502
|
+
channel: stopwatchChannel,
|
|
503
|
+
workspaceId: state.workspaceId,
|
|
504
|
+
nodeId: state.nodeId,
|
|
505
|
+
topics: stopwatchChannel.topicNames,
|
|
506
|
+
});
|
|
507
|
+
// live.latestData → { topic: "tick", data: {...} } — the newest message, or null.
|
|
508
|
+
// live.data → everything received this mount, oldest first.
|
|
509
|
+
|
|
510
|
+
const start = () =>
|
|
511
|
+
kickPluginTask({
|
|
512
|
+
applicationType: "plugin_stopwatch",
|
|
513
|
+
taskName: "tick",
|
|
514
|
+
identifier: state,
|
|
515
|
+
data: { runId, kickedAt: Date.now() },
|
|
516
|
+
});
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
The subscription is per instance and read-only; the token the platform mints for it carries only
|
|
520
|
+
the topics your schema declares. Treat messages as nudges: what the UI must still show after a
|
|
521
|
+
refresh comes from state, so a task that changes anything durable dispatches an event as well
|
|
522
|
+
(docs/07 has the whole loop, including the stop).
|
|
523
|
+
|
|
494
524
|
## Cross-app from the UI
|
|
495
525
|
|
|
496
526
|
```ts
|
|
@@ -628,7 +658,8 @@ tasks: [
|
|
|
628
658
|
- `ctx.getState()` — the app's state, fresh.
|
|
629
659
|
- `ctx.dispatchEvent(eventName, eventData)` — through the full pipeline (append, fold, watermark),
|
|
630
660
|
using YOUR processors, so a task's mutation is identical to a tap's.
|
|
631
|
-
- `ctx.notify(topic, data)` —
|
|
661
|
+
- `ctx.notify(topic, data)` — publish on the app's realtime channel (see *Live tasks* below). A
|
|
662
|
+
nudge, never the truth: the UI hears it while mounted; a refresh only sees the timeline.
|
|
632
663
|
- `ctx.logger`.
|
|
633
664
|
|
|
634
665
|
## The replay model — read this twice
|
|
@@ -642,8 +673,10 @@ replay; mint them inside. Step names are unique per logical operation; loops inc
|
|
|
642
673
|
## How a task gets kicked
|
|
643
674
|
|
|
644
675
|
- **From a webhook**: `ctx.sendInngestEvent("<applicationType>/<task>", payload)` (docs/06).
|
|
645
|
-
- **From the browser**: list the task in `kickableTasks
|
|
646
|
-
|
|
676
|
+
- **From the browser or a tool**: list the task in `kickableTasks`, then
|
|
677
|
+
`kickPluginTask({ applicationType, taskName, identifier, data })` (from `esoul-sdk`; works in
|
|
678
|
+
a component and in a tool's `execute`). The platform's send-event route allows only listed
|
|
679
|
+
tasks and stamps nothing — `data` is exactly what `ctx.eventData` sees.
|
|
647
680
|
- **On a cadence**: `pollTasks: [{ task, everyMinutes }]` — one shared platform sweep kicks each
|
|
648
681
|
live instance at 5-minute granularity (minimum 5). This is your cron. There are deliberately
|
|
649
682
|
no per-app Inngest functions: function ids are fixed at module load, and the plan caps
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "esoul-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Build ExternalSoul apps: event-sourced state, agent tools, server ops, background tasks, OAuth connections, files — the contract the Forge compiles against.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|