@abraca/orchestrator 2.3.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.
@@ -0,0 +1,261 @@
1
+ //#region packages/orchestrator/src/types.d.ts
2
+ /**
3
+ * Type definitions for the orchestrator scene format.
4
+ */
5
+ interface ServerConfig {
6
+ url: string;
7
+ inviteCode?: string;
8
+ }
9
+ interface ActorDef {
10
+ name: string;
11
+ color: string;
12
+ keyFile?: string;
13
+ avatar?: string;
14
+ }
15
+ type EasingName = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';
16
+ interface ConnectAction {
17
+ type: 'connect';
18
+ }
19
+ interface DisconnectAction {
20
+ type: 'disconnect';
21
+ }
22
+ interface NavigateAction {
23
+ type: 'navigate';
24
+ docId: string;
25
+ }
26
+ interface TypeAction {
27
+ type: 'type';
28
+ docId: string;
29
+ text: string;
30
+ /** ms per character (default 80) */
31
+ speed?: number;
32
+ /** random variance +/- ms (default 30) */
33
+ variance?: number;
34
+ /** character index to start typing at (default: end of doc) */
35
+ position?: number;
36
+ }
37
+ interface SelectAction {
38
+ type: 'select';
39
+ docId: string;
40
+ anchor: number;
41
+ head: number;
42
+ }
43
+ interface MoveCursorAction {
44
+ type: 'moveCursor';
45
+ docId: string;
46
+ from: number;
47
+ to: number;
48
+ duration: number;
49
+ easing?: EasingName;
50
+ }
51
+ interface SetStatusAction {
52
+ type: 'setStatus';
53
+ status: string | null;
54
+ }
55
+ interface SetAwarenessAction {
56
+ type: 'setAwareness';
57
+ /** If set, applies to child provider; otherwise root */
58
+ docId?: string;
59
+ fields: Record<string, unknown>;
60
+ }
61
+ interface ClearAwarenessAction {
62
+ type: 'clearAwareness';
63
+ docId?: string;
64
+ fields: string[];
65
+ }
66
+ interface CreateDocumentAction {
67
+ type: 'createDocument';
68
+ parentId: string;
69
+ label: string;
70
+ docType?: string;
71
+ meta?: Record<string, unknown>;
72
+ /** Store created doc ID under this key in vars for later reference */
73
+ assignId?: string;
74
+ }
75
+ interface MoveDocumentAction {
76
+ type: 'moveDocument';
77
+ docId: string;
78
+ newParentId: string;
79
+ order?: number;
80
+ }
81
+ interface WriteContentAction {
82
+ type: 'writeContent';
83
+ docId: string;
84
+ markdown: string;
85
+ }
86
+ interface DeleteContentAction {
87
+ type: 'deleteContent';
88
+ docId: string;
89
+ from: number;
90
+ length: number;
91
+ }
92
+ interface SetMetaAction {
93
+ type: 'setMeta';
94
+ docId: string;
95
+ meta: Record<string, unknown>;
96
+ }
97
+ interface PointerMoveAction {
98
+ type: 'pointerMove';
99
+ docId: string;
100
+ from: {
101
+ x: number;
102
+ y: number;
103
+ };
104
+ to: {
105
+ x: number;
106
+ y: number;
107
+ };
108
+ duration: number;
109
+ easing?: EasingName;
110
+ }
111
+ interface ScrollToAction {
112
+ type: 'scrollTo';
113
+ docId: string;
114
+ /** 0-1 normalized scroll position */
115
+ position: number;
116
+ }
117
+ interface KanbanHoverAction {
118
+ type: 'kanbanHover';
119
+ docId: string;
120
+ cardId: string | null;
121
+ }
122
+ interface KanbanDragAction {
123
+ type: 'kanbanDrag';
124
+ docId: string;
125
+ cardId: string;
126
+ toColumnId: string;
127
+ duration: number;
128
+ }
129
+ interface RenameDocumentAction {
130
+ type: 'renameDocument';
131
+ docId: string;
132
+ label: string;
133
+ }
134
+ interface SendChatAction {
135
+ type: 'sendChat';
136
+ /** Channel key (e.g. 'group:docId' or 'dm:key1:key2') */
137
+ channel: string;
138
+ message: string;
139
+ }
140
+ interface TypeDeleteAction {
141
+ type: 'typeDelete';
142
+ docId: string;
143
+ /** Number of characters to delete (backspace) */
144
+ count: number;
145
+ /** ms per deletion (default 60) */
146
+ speed?: number;
147
+ /** random variance +/- ms (default 20) */
148
+ variance?: number;
149
+ /** character index to start deleting from (default: end of doc) */
150
+ position?: number;
151
+ }
152
+ interface RepeatAction {
153
+ type: 'repeat';
154
+ /** Number of times to repeat */
155
+ times: number;
156
+ actions: TimelineEntry[];
157
+ }
158
+ interface WaitAction {
159
+ type: 'wait';
160
+ duration: number;
161
+ }
162
+ interface ParallelAction {
163
+ type: 'parallel';
164
+ actions: TimelineEntry[];
165
+ }
166
+ interface SequenceAction {
167
+ type: 'sequence';
168
+ actions: TimelineEntry[];
169
+ }
170
+ type Action = ConnectAction | DisconnectAction | NavigateAction | TypeAction | TypeDeleteAction | SelectAction | MoveCursorAction | SetStatusAction | SetAwarenessAction | ClearAwarenessAction | CreateDocumentAction | MoveDocumentAction | RenameDocumentAction | WriteContentAction | DeleteContentAction | SetMetaAction | PointerMoveAction | ScrollToAction | KanbanHoverAction | KanbanDragAction | SendChatAction | WaitAction | ParallelAction | SequenceAction | RepeatAction;
171
+ interface TimelineEntry {
172
+ /** ms from scene start (or parent start for nested entries) */
173
+ at?: number;
174
+ /** Which actor performs this action */
175
+ actor?: string;
176
+ action: Action;
177
+ }
178
+ interface Scene {
179
+ server: ServerConfig;
180
+ actors: ActorDef[];
181
+ timeline: TimelineEntry[];
182
+ /** Pre-defined variables (e.g. known doc IDs) */
183
+ vars?: Record<string, string>;
184
+ /** Max scene duration in ms. Auto-cleanup after this time. */
185
+ duration?: number;
186
+ /** Called after all actors are prepared but before timeline runs. */
187
+ onStart?: () => Promise<void> | void;
188
+ /** Called after timeline completes (before cleanup). */
189
+ onEnd?: () => Promise<void> | void;
190
+ }
191
+ //#endregion
192
+ //#region packages/orchestrator/src/define.d.ts
193
+ /** Define a complete scene. */
194
+ declare function defineScene(scene: Scene): Scene;
195
+ /** Define an actor. */
196
+ declare function actor(name: string, opts: Omit<ActorDef, 'name'>): ActorDef;
197
+ /** Factory functions for all action types. */
198
+ declare const actions: {
199
+ connect(): ConnectAction;
200
+ disconnect(): DisconnectAction;
201
+ navigate(docId: string): NavigateAction;
202
+ type(docId: string, text: string, opts?: {
203
+ speed?: number;
204
+ variance?: number;
205
+ position?: number;
206
+ }): TypeAction;
207
+ typeDelete(docId: string, count: number, opts?: {
208
+ speed?: number;
209
+ variance?: number;
210
+ position?: number;
211
+ }): TypeDeleteAction;
212
+ select(docId: string, anchor: number, head: number): SelectAction;
213
+ moveCursor(docId: string, from: number, to: number, duration: number, easing?: EasingName): MoveCursorAction;
214
+ setStatus(status: string | null): SetStatusAction;
215
+ setAwareness(fields: Record<string, unknown>, docId?: string): SetAwarenessAction;
216
+ clearAwareness(fields: string[], docId?: string): ClearAwarenessAction;
217
+ createDocument(parentId: string, label: string, opts?: {
218
+ docType?: string;
219
+ meta?: Record<string, unknown>;
220
+ assignId?: string;
221
+ }): CreateDocumentAction;
222
+ moveDocument(docId: string, newParentId: string, order?: number): MoveDocumentAction;
223
+ renameDocument(docId: string, label: string): RenameDocumentAction;
224
+ writeContent(docId: string, markdown: string): WriteContentAction;
225
+ deleteContent(docId: string, from: number, length: number): DeleteContentAction;
226
+ setMeta(docId: string, meta: Record<string, unknown>): SetMetaAction;
227
+ pointerMove(docId: string, from: {
228
+ x: number;
229
+ y: number;
230
+ }, to: {
231
+ x: number;
232
+ y: number;
233
+ }, duration: number, easing?: EasingName): PointerMoveAction;
234
+ scrollTo(docId: string, position: number): ScrollToAction;
235
+ kanbanHover(docId: string, cardId: string | null): KanbanHoverAction;
236
+ kanbanDrag(docId: string, cardId: string, toColumnId: string, duration: number): KanbanDragAction;
237
+ sendChat(channel: string, message: string): SendChatAction;
238
+ wait(duration: number): WaitAction;
239
+ parallel(entries: TimelineEntry[]): ParallelAction;
240
+ sequence(entries: TimelineEntry[]): SequenceAction;
241
+ repeat(times: number, entries: TimelineEntry[]): RepeatAction;
242
+ };
243
+ //#endregion
244
+ //#region packages/orchestrator/src/orchestrator.d.ts
245
+ declare class Orchestrator {
246
+ private scene;
247
+ private actors;
248
+ private vars;
249
+ /** Load a scene from a TypeScript file. */
250
+ load(scriptPath: string): Promise<void>;
251
+ /** Prepare actor connections (does not connect yet — that's a timeline action). */
252
+ prepare(): void;
253
+ /** Validate the scene without connecting. Logs the timeline structure. */
254
+ dryRun(): void;
255
+ /** Run the timeline. */
256
+ run(): Promise<void>;
257
+ /** Disconnect all actors gracefully. */
258
+ cleanup(): Promise<void>;
259
+ }
260
+ //#endregion
261
+ export { type Action, type ActorDef, type ClearAwarenessAction, type ConnectAction, type CreateDocumentAction, type DeleteContentAction, type DisconnectAction, type EasingName, type KanbanDragAction, type KanbanHoverAction, type MoveCursorAction, type MoveDocumentAction, type NavigateAction, Orchestrator, type ParallelAction, type PointerMoveAction, type RenameDocumentAction, type RepeatAction, type Scene, type ScrollToAction, type SelectAction, type SendChatAction, type SequenceAction, type ServerConfig, type SetAwarenessAction, type SetMetaAction, type SetStatusAction, type TimelineEntry, type TypeAction, type TypeDeleteAction, type WaitAction, type WriteContentAction, actions, actor, defineScene };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@abraca/orchestrator",
3
+ "version": "2.3.0",
4
+ "description": "CouShell commercial director — orchestrate simulated actors on Abracadabra for screen recording",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/abracadabra-orchestrator.cjs",
8
+ "module": "dist/abracadabra-orchestrator.esm.js",
9
+ "types": "dist/index.d.ts",
10
+ "bin": {
11
+ "abracadabra-orchestrator": "./dist/abracadabra-orchestrator.esm.js"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "exports": {
17
+ "source": {
18
+ "import": "./src/index.ts"
19
+ },
20
+ "default": {
21
+ "import": "./dist/abracadabra-orchestrator.esm.js",
22
+ "require": "./dist/abracadabra-orchestrator.cjs",
23
+ "types": "./dist/index.d.ts"
24
+ }
25
+ },
26
+ "files": [
27
+ "src",
28
+ "dist"
29
+ ],
30
+ "dependencies": {
31
+ "@noble/ed25519": "^3.1.0",
32
+ "@noble/hashes": "^2.2.0"
33
+ },
34
+ "peerDependencies": {
35
+ "@abraca/dabra": ">=1.0.0",
36
+ "y-protocols": "^1.0.6",
37
+ "yjs": "^13.6.8"
38
+ }
39
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Awareness actions: setStatus, setAwareness, clearAwareness, pointerMove, scrollTo,
3
+ * kanbanHover, kanbanDrag.
4
+ */
5
+ import type { ActorConnection } from '../actor-connection.ts'
6
+ import type {
7
+ SetStatusAction,
8
+ SetAwarenessAction,
9
+ ClearAwarenessAction,
10
+ PointerMoveAction,
11
+ ScrollToAction,
12
+ KanbanHoverAction,
13
+ KanbanDragAction,
14
+ } from '../types.ts'
15
+ import { sleep } from '../utils.ts'
16
+ import { getEasing } from '../easing.ts'
17
+
18
+ export async function executeSetStatus(
19
+ actor: ActorConnection,
20
+ action: SetStatusAction
21
+ ): Promise<void> {
22
+ actor.setRootAwareness('status', action.status)
23
+ }
24
+
25
+ export async function executeSetAwareness(
26
+ actor: ActorConnection,
27
+ action: SetAwarenessAction
28
+ ): Promise<void> {
29
+ if (action.docId) {
30
+ // Ensure child provider is loaded
31
+ await actor.getChildProvider(action.docId)
32
+ for (const [key, value] of Object.entries(action.fields)) {
33
+ actor.setChildAwareness(action.docId, key, value)
34
+ }
35
+ } else {
36
+ for (const [key, value] of Object.entries(action.fields)) {
37
+ actor.setRootAwareness(key, value)
38
+ }
39
+ }
40
+ }
41
+
42
+ export async function executeClearAwareness(
43
+ actor: ActorConnection,
44
+ action: ClearAwarenessAction
45
+ ): Promise<void> {
46
+ if (action.docId) {
47
+ for (const field of action.fields) {
48
+ actor.setChildAwareness(action.docId, field, null)
49
+ }
50
+ } else {
51
+ for (const field of action.fields) {
52
+ actor.setRootAwareness(field, null)
53
+ }
54
+ }
55
+ }
56
+
57
+ export async function executePointerMove(
58
+ actor: ActorConnection,
59
+ action: PointerMoveAction
60
+ ): Promise<void> {
61
+ const easing = getEasing(action.easing)
62
+ const steps = Math.max(1, Math.round(action.duration / 16)) // ~60fps
63
+ const stepDuration = action.duration / steps
64
+
65
+ for (let i = 0; i <= steps; i++) {
66
+ const t = easing(i / steps)
67
+ const x = action.from.x + (action.to.x - action.from.x) * t
68
+ const y = action.from.y + (action.to.y - action.from.y) * t
69
+ actor.setChildAwareness(action.docId, 'pos', { x, y })
70
+ if (i < steps) await sleep(stepDuration)
71
+ }
72
+ }
73
+
74
+ export async function executeScrollTo(
75
+ actor: ActorConnection,
76
+ action: ScrollToAction
77
+ ): Promise<void> {
78
+ actor.setChildAwareness(action.docId, 'doc:scroll', action.position)
79
+ }
80
+
81
+ export async function executeKanbanHover(
82
+ actor: ActorConnection,
83
+ action: KanbanHoverAction
84
+ ): Promise<void> {
85
+ actor.setChildAwareness(action.docId, 'kanban:hovering', action.cardId)
86
+ }
87
+
88
+ export async function executeKanbanDrag(
89
+ actor: ActorConnection,
90
+ action: KanbanDragAction
91
+ ): Promise<void> {
92
+ // Set drag state
93
+ actor.setChildAwareness(action.docId, 'kanban:dragging', {
94
+ cardId: action.cardId,
95
+ toColumnId: action.toColumnId,
96
+ })
97
+ // Hold for duration
98
+ await sleep(action.duration)
99
+ // Clear drag state
100
+ actor.setChildAwareness(action.docId, 'kanban:dragging', null)
101
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Chat action — send a stateless message via the provider.
3
+ *
4
+ * Routes through the unified `messages:send` protocol. `action.channel` is
5
+ * interpreted as a doc id (the channel/dm doc UUID); legacy `group:<docId>`
6
+ * strings are accepted with the prefix stripped.
7
+ */
8
+ import type { ActorConnection } from '../actor-connection.ts'
9
+ import type { SendChatAction } from '../types.ts'
10
+
11
+ export async function executeSendChat(
12
+ actor: ActorConnection,
13
+ action: SendChatAction
14
+ ): Promise<void> {
15
+ const rootProvider = actor.rootProvider
16
+ if (!rootProvider) throw new Error(`${actor.actor.name}: not connected`)
17
+
18
+ const channel_doc_id = action.channel.startsWith('group:')
19
+ ? action.channel.slice(6)
20
+ : action.channel
21
+
22
+ const payload = JSON.stringify({
23
+ type: 'messages:send',
24
+ channel_doc_id,
25
+ content: action.message,
26
+ mentions: [],
27
+ })
28
+
29
+ rootProvider.sendStateless(payload)
30
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Connect / disconnect actions.
3
+ */
4
+ import type { ActorConnection } from '../actor-connection.ts'
5
+ import type { ServerConfig } from '../types.ts'
6
+
7
+ export async function executeConnect(
8
+ actor: ActorConnection,
9
+ serverConfig: ServerConfig
10
+ ): Promise<void> {
11
+ await actor.connect(serverConfig)
12
+ }
13
+
14
+ export async function executeDisconnect(actor: ActorConnection): Promise<void> {
15
+ await actor.disconnect()
16
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Content actions: writeContent (markdown → Y.js), deleteContent.
3
+ *
4
+ * Preserves TipTap schema nodes (documentHeader, documentMeta) —
5
+ * only body content is cleared/replaced.
6
+ */
7
+ import type { ActorConnection } from '../actor-connection.ts'
8
+ import type { WriteContentAction, DeleteContentAction } from '../types.ts'
9
+ import { populateYDocFromMarkdown } from '../converters/markdownToYjs.ts'
10
+ import { bodyStartIndex } from '../yjs-utils.ts'
11
+
12
+ export async function executeWriteContent(
13
+ actor: ActorConnection,
14
+ action: WriteContentAction
15
+ ): Promise<void> {
16
+ const provider = await actor.getChildProvider(action.docId)
17
+ const fragment = provider.document.getXmlFragment('default')
18
+
19
+ // Clear entire fragment (schema nodes + body) — populateYDocFromMarkdown rebuilds them
20
+ provider.document.transact(() => {
21
+ while (fragment.length > 0) {
22
+ fragment.delete(0, 1)
23
+ }
24
+ })
25
+
26
+ // Populate from markdown (creates fresh documentHeader + documentMeta + body)
27
+ populateYDocFromMarkdown(fragment, action.markdown)
28
+ }
29
+
30
+ export async function executeDeleteContent(
31
+ actor: ActorConnection,
32
+ action: DeleteContentAction
33
+ ): Promise<void> {
34
+ const provider = await actor.getChildProvider(action.docId)
35
+ const fragment = provider.document.getXmlFragment('default')
36
+
37
+ provider.document.transact(() => {
38
+ // Offset from by the schema nodes to protect them
39
+ const start = bodyStartIndex(fragment)
40
+ const adjustedFrom = start + action.from
41
+ const count = Math.min(action.length, fragment.length - adjustedFrom)
42
+ if (count > 0) {
43
+ fragment.delete(adjustedFrom, count)
44
+ }
45
+ })
46
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Cursor actions: moveCursor (animated), select.
3
+ */
4
+ import type { ActorConnection } from '../actor-connection.ts'
5
+ import type { MoveCursorAction, SelectAction } from '../types.ts'
6
+ import { sleep } from '../utils.ts'
7
+ import { getEasing } from '../easing.ts'
8
+
9
+ export async function executeMoveCursor(
10
+ actor: ActorConnection,
11
+ action: MoveCursorAction
12
+ ): Promise<void> {
13
+ const easing = getEasing(action.easing)
14
+ const steps = Math.max(1, Math.round(action.duration / 16))
15
+ const stepDuration = action.duration / steps
16
+
17
+ for (let i = 0; i <= steps; i++) {
18
+ const t = easing(i / steps)
19
+ const pos = Math.round(action.from + (action.to - action.from) * t)
20
+ actor.setCursor(action.docId, pos)
21
+ if (i < steps) await sleep(stepDuration)
22
+ }
23
+ }
24
+
25
+ export async function executeSelect(
26
+ actor: ActorConnection,
27
+ action: SelectAction
28
+ ): Promise<void> {
29
+ actor.setSelection(action.docId, action.anchor, action.head)
30
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Document management actions: createDocument, moveDocument, setMeta.
3
+ * These operate on the Y.js doc-tree map (same as the dashboard and MCP do).
4
+ */
5
+ import type { ActorConnection } from '../actor-connection.ts'
6
+ import type { CreateDocumentAction, MoveDocumentAction, SetMetaAction, RenameDocumentAction } from '../types.ts'
7
+ import { log } from '../utils.ts'
8
+
9
+ export async function executeCreateDocument(
10
+ actor: ActorConnection,
11
+ action: CreateDocumentAction,
12
+ vars: Map<string, string>
13
+ ): Promise<void> {
14
+ const rootProvider = actor.rootProvider
15
+ if (!rootProvider) throw new Error(`${actor.actor.name}: not connected`)
16
+
17
+ const treeMap = rootProvider.document.getMap('doc-tree')
18
+ const id = crypto.randomUUID()
19
+ const now = Date.now()
20
+
21
+ // Normalize parentId: if it matches the root doc, use null (top-level)
22
+ const normalizedParent = action.parentId === actor.rootDocId ? null : action.parentId
23
+
24
+ rootProvider.document.transact(() => {
25
+ treeMap.set(id, {
26
+ label: action.label,
27
+ parentId: normalizedParent,
28
+ order: now,
29
+ type: action.docType,
30
+ meta: action.meta,
31
+ createdAt: now,
32
+ updatedAt: now,
33
+ })
34
+ })
35
+
36
+ log(`${actor.actor.name}: created document "${action.label}" (${id})`)
37
+
38
+ // Store the created ID for later reference in the script
39
+ if (action.assignId) {
40
+ vars.set(action.assignId, id)
41
+ }
42
+ }
43
+
44
+ export async function executeMoveDocument(
45
+ actor: ActorConnection,
46
+ action: MoveDocumentAction
47
+ ): Promise<void> {
48
+ const rootProvider = actor.rootProvider
49
+ if (!rootProvider) throw new Error(`${actor.actor.name}: not connected`)
50
+
51
+ const treeMap = rootProvider.document.getMap('doc-tree')
52
+ const entry = treeMap.get(action.docId) as Record<string, unknown> | undefined
53
+ if (!entry) {
54
+ log(`${actor.actor.name}: document ${action.docId} not found in tree`)
55
+ return
56
+ }
57
+
58
+ // Normalize parentId
59
+ const normalizedParent = action.newParentId === actor.rootDocId ? null : action.newParentId
60
+
61
+ rootProvider.document.transact(() => {
62
+ treeMap.set(action.docId, {
63
+ ...entry,
64
+ parentId: normalizedParent,
65
+ order: action.order ?? Date.now(),
66
+ updatedAt: Date.now(),
67
+ })
68
+ })
69
+
70
+ log(`${actor.actor.name}: moved document ${action.docId} to ${action.newParentId}`)
71
+ }
72
+
73
+ export async function executeSetMeta(
74
+ actor: ActorConnection,
75
+ action: SetMetaAction
76
+ ): Promise<void> {
77
+ const rootProvider = actor.rootProvider
78
+ if (!rootProvider) throw new Error(`${actor.actor.name}: not connected`)
79
+
80
+ const treeMap = rootProvider.document.getMap('doc-tree')
81
+ const entry = treeMap.get(action.docId) as Record<string, unknown> | undefined
82
+ if (!entry) {
83
+ log(`${actor.actor.name}: document ${action.docId} not found in tree`)
84
+ return
85
+ }
86
+
87
+ rootProvider.document.transact(() => {
88
+ const currentMeta = (entry.meta as Record<string, unknown>) ?? {}
89
+ treeMap.set(action.docId, {
90
+ ...entry,
91
+ meta: { ...currentMeta, ...action.meta },
92
+ updatedAt: Date.now(),
93
+ })
94
+ })
95
+ }
96
+
97
+ export async function executeRenameDocument(
98
+ actor: ActorConnection,
99
+ action: RenameDocumentAction
100
+ ): Promise<void> {
101
+ const rootProvider = actor.rootProvider
102
+ if (!rootProvider) throw new Error(`${actor.actor.name}: not connected`)
103
+
104
+ const treeMap = rootProvider.document.getMap('doc-tree')
105
+ const entry = treeMap.get(action.docId) as Record<string, unknown> | undefined
106
+ if (!entry) {
107
+ log(`${actor.actor.name}: document ${action.docId} not found in tree`)
108
+ return
109
+ }
110
+
111
+ rootProvider.document.transact(() => {
112
+ treeMap.set(action.docId, {
113
+ ...entry,
114
+ label: action.label,
115
+ updatedAt: Date.now(),
116
+ })
117
+ })
118
+
119
+ log(`${actor.actor.name}: renamed ${action.docId} to "${action.label}"`)
120
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Flow control actions: wait, parallel, sequence.
3
+ */
4
+ import type { WaitAction, ParallelAction, SequenceAction, RepeatAction, TimelineEntry } from '../types.ts'
5
+ import { sleep } from '../utils.ts'
6
+ import type { ActionExecutor } from './index.ts'
7
+
8
+ export async function executeWait(action: WaitAction): Promise<void> {
9
+ await sleep(action.duration)
10
+ }
11
+
12
+ export async function executeParallel(
13
+ action: ParallelAction,
14
+ execute: ActionExecutor
15
+ ): Promise<void> {
16
+ const promises = action.actions.map(async (entry) => {
17
+ const delay = entry.at ?? 0
18
+ if (delay > 0) await sleep(delay)
19
+ await execute(entry)
20
+ })
21
+ await Promise.all(promises)
22
+ }
23
+
24
+ export async function executeSequence(
25
+ action: SequenceAction,
26
+ execute: ActionExecutor
27
+ ): Promise<void> {
28
+ for (const entry of action.actions) {
29
+ const delay = entry.at ?? 0
30
+ if (delay > 0) await sleep(delay)
31
+ await execute(entry)
32
+ }
33
+ }
34
+
35
+ export async function executeRepeat(
36
+ action: RepeatAction,
37
+ execute: ActionExecutor
38
+ ): Promise<void> {
39
+ for (let n = 0; n < action.times; n++) {
40
+ for (const entry of action.actions) {
41
+ const delay = entry.at ?? 0
42
+ if (delay > 0) await sleep(delay)
43
+ await execute(entry)
44
+ }
45
+ }
46
+ }