laive-mcp 0.1.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/AGENTS.md +48 -0
- package/CHANGELOG.md +13 -0
- package/LICENSE +674 -0
- package/README.md +219 -0
- package/bin/laive.mjs +340 -0
- package/package.json +66 -0
- package/packages/als-parser/src/index.js +2 -0
- package/packages/als-parser/src/read.js +16 -0
- package/packages/als-parser/src/summarize.js +116 -0
- package/packages/common/src/index.js +3 -0
- package/packages/common/src/jsonl.js +41 -0
- package/packages/common/src/protocol.js +94 -0
- package/packages/common/src/validation.js +121 -0
- package/packages/live-bridge-remote-script/README.md +22 -0
- package/packages/live-bridge-remote-script/python/laive/__init__.py +7 -0
- package/packages/live-bridge-remote-script/python/laive/control_surface.py +208 -0
- package/packages/live-bridge-remote-script/python/laive/fake_live.py +168 -0
- package/packages/live-bridge-remote-script/python/laive/listeners.py +46 -0
- package/packages/live-bridge-remote-script/python/laive/live_access.py +272 -0
- package/packages/live-bridge-remote-script/python/laive/protocol.py +87 -0
- package/packages/live-bridge-remote-script/python/laive/server.py +130 -0
- package/packages/live-bridge-remote-script/python/laive/task_queue.py +47 -0
- package/packages/live-bridge-remote-script/src/bridge/client.js +113 -0
- package/packages/live-bridge-remote-script/src/bridge/server.js +189 -0
- package/packages/live-bridge-remote-script/src/cli/client.js +75 -0
- package/packages/live-bridge-remote-script/src/cli/server.js +51 -0
- package/packages/live-bridge-remote-script/src/fixtures/default-live-set.json +113 -0
- package/packages/live-bridge-remote-script/src/index.js +3 -0
- package/packages/live-bridge-remote-script/src/runtime/fixture-runtime.js +356 -0
- package/packages/live-sidecar-m4l/README.md +45 -0
- package/packages/live-sidecar-m4l/device/laive-sidecar.amxd +0 -0
- package/packages/live-sidecar-m4l/project/code/laive-sidecar-node.js +149 -0
- package/packages/live-sidecar-m4l/project/data/laive-sidecar.manifest.json +8 -0
- package/packages/live-sidecar-m4l/project/laive-sidecar.maxproj +36 -0
- package/packages/live-sidecar-m4l/project/patchers/laive-sidecar.maxpat +172 -0
- package/packages/live-sidecar-m4l/src/contracts.js +35 -0
- package/packages/live-sidecar-m4l/src/index.js +19 -0
- package/packages/live-sidecar-m4l/src/install-sidecar-device.js +15 -0
- package/packages/live-sidecar-m4l/src/package-sidecar.js +5 -0
- package/packages/live-sidecar-m4l/src/project.js +132 -0
- package/packages/live-sidecar-m4l/src/runtime.js +96 -0
- package/packages/live-sidecar-m4l/src/workflows.js +95 -0
- package/packages/mcp-server/src/cli.js +113 -0
- package/packages/mcp-server/src/default-tools.js +253 -0
- package/packages/mcp-server/src/errors.js +24 -0
- package/packages/mcp-server/src/index.js +10 -0
- package/packages/mcp-server/src/server.js +96 -0
- package/packages/mcp-server/src/session.js +475 -0
- package/packages/mcp-server/src/tool-registry.js +41 -0
- package/packages/state-engine/src/engine.js +566 -0
- package/packages/state-engine/src/ids.js +57 -0
- package/packages/state-engine/src/index.js +40 -0
- package/packages/state-engine/src/normalize.js +357 -0
- package/packages/state-engine/src/queries.js +154 -0
- package/packages/state-engine/src/replay.js +60 -0
- package/packages/ui-automation/src/executor.js +87 -0
- package/packages/ui-automation/src/guards.js +21 -0
- package/packages/ui-automation/src/helper.js +186 -0
- package/packages/ui-automation/src/index.js +20 -0
- package/packages/ui-automation/src/macos.js +82 -0
- package/packages/ui-automation/src/package-ui-helper.js +5 -0
- package/packages/ui-automation/src/workflows.js +72 -0
- package/scripts/install-remote-script.py +7 -0
- package/scripts/install-ui-helper.mjs +14 -0
- package/scripts/package-remote-script.py +7 -0
- package/scripts/package-ui-helper.mjs +4 -0
- package/scripts/remote_script_tooling.py +253 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compareTrackIds,
|
|
3
|
+
makeArrangementClipId,
|
|
4
|
+
makeDeviceId,
|
|
5
|
+
makeParameterId,
|
|
6
|
+
makeSceneId,
|
|
7
|
+
makeSessionClipId,
|
|
8
|
+
makeTrackId
|
|
9
|
+
} from "./ids.js";
|
|
10
|
+
import {
|
|
11
|
+
normalizeApplication,
|
|
12
|
+
normalizeCapabilities,
|
|
13
|
+
normalizeClip,
|
|
14
|
+
normalizeDevice,
|
|
15
|
+
normalizeParameter,
|
|
16
|
+
normalizeScene,
|
|
17
|
+
normalizeSelection,
|
|
18
|
+
normalizeSong,
|
|
19
|
+
normalizeTrack,
|
|
20
|
+
normalizeTrackBundle
|
|
21
|
+
} from "./normalize.js";
|
|
22
|
+
import {
|
|
23
|
+
findTrack,
|
|
24
|
+
getSelectedContext,
|
|
25
|
+
getTrackDetails,
|
|
26
|
+
listPlayingClips,
|
|
27
|
+
searchEntities,
|
|
28
|
+
summarizeProject
|
|
29
|
+
} from "./queries.js";
|
|
30
|
+
|
|
31
|
+
function isoNow(value) {
|
|
32
|
+
if (typeof value === "string" && value.length > 0) {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return new Date().toISOString();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createEmptyEntityCollections() {
|
|
40
|
+
return {
|
|
41
|
+
tracks: {},
|
|
42
|
+
scenes: {},
|
|
43
|
+
clips: {},
|
|
44
|
+
devices: {},
|
|
45
|
+
parameters: {}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createInitialState(options = {}) {
|
|
50
|
+
const observedAt = isoNow(options.observedAt);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
meta: {
|
|
54
|
+
snapshotVersion: 0,
|
|
55
|
+
lastUpdatedAt: observedAt,
|
|
56
|
+
lastEventAt: null,
|
|
57
|
+
bridgeVersion: null,
|
|
58
|
+
liveVersion: null,
|
|
59
|
+
dirtyPaths: [],
|
|
60
|
+
traceLength: 0
|
|
61
|
+
},
|
|
62
|
+
application: null,
|
|
63
|
+
song: null,
|
|
64
|
+
selection: null,
|
|
65
|
+
capabilities: null,
|
|
66
|
+
trackOrder: [],
|
|
67
|
+
visibleTrackIds: [],
|
|
68
|
+
returnTrackIds: [],
|
|
69
|
+
masterTrackId: null,
|
|
70
|
+
sceneOrder: [],
|
|
71
|
+
...createEmptyEntityCollections()
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sortedUniqueStrings(values) {
|
|
76
|
+
return [...new Set(values.filter(Boolean))].sort();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function replaceDirtyPaths(state, dirtyPaths) {
|
|
80
|
+
state.meta.dirtyPaths = sortedUniqueStrings(dirtyPaths);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function addDirtyPaths(state, dirtyPaths = []) {
|
|
84
|
+
replaceDirtyPaths(state, [...state.meta.dirtyPaths, ...dirtyPaths]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function clearDirtyPaths(state, dirtyPaths = []) {
|
|
88
|
+
const dirtySet = new Set(state.meta.dirtyPaths);
|
|
89
|
+
|
|
90
|
+
for (const dirtyPath of dirtyPaths) {
|
|
91
|
+
dirtySet.delete(dirtyPath);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
state.meta.dirtyPaths = [...dirtySet].sort();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function updateMetaAfterMutation(state, options = {}) {
|
|
98
|
+
const observedAt = isoNow(options.observedAt);
|
|
99
|
+
state.meta.snapshotVersion += 1;
|
|
100
|
+
state.meta.lastUpdatedAt = observedAt;
|
|
101
|
+
state.meta.traceLength += 1;
|
|
102
|
+
|
|
103
|
+
if (options.lastEventAt) {
|
|
104
|
+
state.meta.lastEventAt = isoNow(options.lastEventAt);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (options.bridgeVersion) {
|
|
108
|
+
state.meta.bridgeVersion = options.bridgeVersion;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (options.liveVersion) {
|
|
112
|
+
state.meta.liveVersion = options.liveVersion;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function removeParametersForDevice(state, deviceId) {
|
|
117
|
+
const device = state.devices[deviceId];
|
|
118
|
+
|
|
119
|
+
if (!device) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const parameterId of device.parameterIds) {
|
|
124
|
+
delete state.parameters[parameterId];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function removeDevice(state, deviceId) {
|
|
129
|
+
removeParametersForDevice(state, deviceId);
|
|
130
|
+
delete state.devices[deviceId];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function removeTrackSubtree(state, trackId) {
|
|
134
|
+
const track = state.tracks[trackId];
|
|
135
|
+
|
|
136
|
+
if (!track) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const clipId of [...track.sessionClipIds, ...track.arrangementClipIds]) {
|
|
141
|
+
delete state.clips[clipId];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const deviceId of track.deviceIds) {
|
|
145
|
+
removeDevice(state, deviceId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
delete state.tracks[trackId];
|
|
149
|
+
state.trackOrder = state.trackOrder.filter((currentTrackId) => currentTrackId !== trackId);
|
|
150
|
+
state.visibleTrackIds = state.visibleTrackIds.filter(
|
|
151
|
+
(currentTrackId) => currentTrackId !== trackId
|
|
152
|
+
);
|
|
153
|
+
state.returnTrackIds = state.returnTrackIds.filter(
|
|
154
|
+
(currentTrackId) => currentTrackId !== trackId
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (state.masterTrackId === trackId) {
|
|
158
|
+
state.masterTrackId = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function upsertTrackBundle(state, track, options = {}) {
|
|
163
|
+
const trackId =
|
|
164
|
+
track.id ??
|
|
165
|
+
makeTrackId(track.section ?? track.kind ?? "visible", track.index ?? track.track_index ?? 0);
|
|
166
|
+
const bundle = normalizeTrackBundle(
|
|
167
|
+
track,
|
|
168
|
+
{
|
|
169
|
+
track: state.tracks[trackId],
|
|
170
|
+
clips: state.clips,
|
|
171
|
+
devices: state.devices,
|
|
172
|
+
parameters: state.parameters
|
|
173
|
+
},
|
|
174
|
+
options
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const previousTrack = state.tracks[bundle.track.id];
|
|
178
|
+
|
|
179
|
+
if (previousTrack) {
|
|
180
|
+
const previousClipIds = new Set([
|
|
181
|
+
...previousTrack.sessionClipIds,
|
|
182
|
+
...previousTrack.arrangementClipIds
|
|
183
|
+
]);
|
|
184
|
+
const nextClipIds = new Set([
|
|
185
|
+
...bundle.track.sessionClipIds,
|
|
186
|
+
...bundle.track.arrangementClipIds
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
for (const clipId of previousClipIds) {
|
|
190
|
+
if (!nextClipIds.has(clipId)) {
|
|
191
|
+
delete state.clips[clipId];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const previousDeviceIds = new Set(previousTrack.deviceIds);
|
|
196
|
+
const nextDeviceIds = new Set(bundle.track.deviceIds);
|
|
197
|
+
|
|
198
|
+
for (const deviceId of previousDeviceIds) {
|
|
199
|
+
if (!nextDeviceIds.has(deviceId)) {
|
|
200
|
+
removeDevice(state, deviceId);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
state.tracks[bundle.track.id] = bundle.track;
|
|
206
|
+
|
|
207
|
+
for (const clip of [...bundle.sessionClips, ...bundle.arrangementClips]) {
|
|
208
|
+
state.clips[clip.id] = clip;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const { device, parameters } of bundle.devices) {
|
|
212
|
+
state.devices[device.id] = device;
|
|
213
|
+
|
|
214
|
+
for (const parameter of parameters) {
|
|
215
|
+
state.parameters[parameter.id] = parameter;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const trackOrderSet = new Set(state.trackOrder);
|
|
220
|
+
trackOrderSet.add(bundle.track.id);
|
|
221
|
+
state.trackOrder = [...trackOrderSet].sort(compareTrackIds);
|
|
222
|
+
|
|
223
|
+
if (bundle.track.section === "visible") {
|
|
224
|
+
state.visibleTrackIds = [...new Set([...state.visibleTrackIds, bundle.track.id])].sort(
|
|
225
|
+
compareTrackIds
|
|
226
|
+
);
|
|
227
|
+
} else if (bundle.track.section === "return") {
|
|
228
|
+
state.returnTrackIds = [...new Set([...state.returnTrackIds, bundle.track.id])].sort(
|
|
229
|
+
compareTrackIds
|
|
230
|
+
);
|
|
231
|
+
} else if (bundle.track.section === "master") {
|
|
232
|
+
state.masterTrackId = bundle.track.id;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function upsertScene(state, scene, options = {}) {
|
|
237
|
+
const sceneId = scene.id ?? makeSceneId(scene.index ?? scene.scene_index ?? 0);
|
|
238
|
+
const normalizedScene = normalizeScene(scene, state.scenes[sceneId], options);
|
|
239
|
+
state.scenes[normalizedScene.id] = normalizedScene;
|
|
240
|
+
state.sceneOrder = [...new Set([...state.sceneOrder, normalizedScene.id])].sort(
|
|
241
|
+
(left, right) => state.scenes[left].index - state.scenes[right].index
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function upsertClip(state, clip, options = {}) {
|
|
246
|
+
const clipId =
|
|
247
|
+
clip.id ??
|
|
248
|
+
(clip.location === "arrangement"
|
|
249
|
+
? makeArrangementClipId(
|
|
250
|
+
clip.track_id,
|
|
251
|
+
clip.index ?? clip.arrangement_index ?? clip.arrangementIndex ?? 0
|
|
252
|
+
)
|
|
253
|
+
: makeSessionClipId(
|
|
254
|
+
clip.track_id,
|
|
255
|
+
clip.slot_index ?? clip.slotIndex ?? clip.scene_index ?? 0
|
|
256
|
+
));
|
|
257
|
+
const normalizedClip = normalizeClip(
|
|
258
|
+
clip,
|
|
259
|
+
clip.track_id,
|
|
260
|
+
state.clips[clipId],
|
|
261
|
+
options
|
|
262
|
+
);
|
|
263
|
+
state.clips[normalizedClip.id] = normalizedClip;
|
|
264
|
+
|
|
265
|
+
const track = state.tracks[normalizedClip.trackId];
|
|
266
|
+
|
|
267
|
+
if (track) {
|
|
268
|
+
if (normalizedClip.location === "session") {
|
|
269
|
+
track.sessionClipIds = [...new Set([...track.sessionClipIds, normalizedClip.id])];
|
|
270
|
+
} else {
|
|
271
|
+
track.arrangementClipIds = [...new Set([...track.arrangementClipIds, normalizedClip.id])];
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function upsertDevice(state, device, options = {}) {
|
|
277
|
+
const deviceId =
|
|
278
|
+
device.id ?? makeDeviceId(device.track_id, device.index ?? device.device_index ?? 0);
|
|
279
|
+
const normalizedDevice = normalizeDevice(
|
|
280
|
+
device,
|
|
281
|
+
device.track_id,
|
|
282
|
+
state.devices[deviceId],
|
|
283
|
+
options
|
|
284
|
+
);
|
|
285
|
+
state.devices[normalizedDevice.id] = normalizedDevice;
|
|
286
|
+
|
|
287
|
+
const track = state.tracks[normalizedDevice.trackId];
|
|
288
|
+
|
|
289
|
+
if (track) {
|
|
290
|
+
track.deviceIds = [...new Set([...track.deviceIds, normalizedDevice.id])];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function upsertParameter(state, parameter, options = {}) {
|
|
295
|
+
const parameterId =
|
|
296
|
+
parameter.id ??
|
|
297
|
+
makeParameterId(
|
|
298
|
+
parameter.device_id,
|
|
299
|
+
parameter.index ?? parameter.parameter_index ?? parameter.parameterIndex ?? 0
|
|
300
|
+
);
|
|
301
|
+
const normalizedParameter = normalizeParameter(
|
|
302
|
+
parameter,
|
|
303
|
+
parameter.device_id,
|
|
304
|
+
state.parameters[parameterId],
|
|
305
|
+
options
|
|
306
|
+
);
|
|
307
|
+
state.parameters[normalizedParameter.id] = normalizedParameter;
|
|
308
|
+
|
|
309
|
+
const device = state.devices[normalizedParameter.deviceId];
|
|
310
|
+
|
|
311
|
+
if (device) {
|
|
312
|
+
device.parameterIds = [...new Set([...device.parameterIds, normalizedParameter.id])];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function applySnapshot(state, snapshot, options = {}) {
|
|
317
|
+
const nextState = createInitialState({
|
|
318
|
+
observedAt: options.observedAt ?? snapshot.observed_at
|
|
319
|
+
});
|
|
320
|
+
nextState.meta.snapshotVersion = state.meta.snapshotVersion;
|
|
321
|
+
nextState.meta.traceLength = state.meta.traceLength;
|
|
322
|
+
nextState.meta.lastEventAt = state.meta.lastEventAt;
|
|
323
|
+
|
|
324
|
+
if (snapshot.application) {
|
|
325
|
+
nextState.application = normalizeApplication(snapshot.application, null, options);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (snapshot.song) {
|
|
329
|
+
nextState.song = normalizeSong(snapshot.song, null, options);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (snapshot.selection) {
|
|
333
|
+
nextState.selection = normalizeSelection(snapshot.selection, null, options);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (snapshot.capabilities) {
|
|
337
|
+
nextState.capabilities = normalizeCapabilities(snapshot.capabilities, null, options);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
for (const scene of snapshot.scenes ?? []) {
|
|
341
|
+
upsertScene(nextState, scene, options);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
for (const track of snapshot.tracks ?? []) {
|
|
345
|
+
upsertTrackBundle(nextState, track, options);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
updateMetaAfterMutation(nextState, {
|
|
349
|
+
observedAt: options.observedAt ?? snapshot.observed_at,
|
|
350
|
+
bridgeVersion: snapshot.bridge_version ?? options.bridgeVersion,
|
|
351
|
+
liveVersion: snapshot.live_version ?? options.liveVersion
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
replaceDirtyPaths(nextState, []);
|
|
355
|
+
return nextState;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function handleTransportChanged(state, payload, options) {
|
|
359
|
+
state.song = normalizeSong({ ...state.song, ...payload }, state.song, options);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function handleSelectionChanged(state, payload, options) {
|
|
363
|
+
state.selection = normalizeSelection(payload, state.selection, options);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function handleTrackRemoved(state, payload) {
|
|
367
|
+
const trackId = payload.track_id ?? payload.id;
|
|
368
|
+
|
|
369
|
+
if (!trackId) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
removeTrackSubtree(state, trackId);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function handleSceneRemoved(state, payload) {
|
|
377
|
+
const sceneId = payload.scene_id ?? payload.id;
|
|
378
|
+
|
|
379
|
+
if (!sceneId) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
delete state.scenes[sceneId];
|
|
384
|
+
state.sceneOrder = state.sceneOrder.filter((currentSceneId) => currentSceneId !== sceneId);
|
|
385
|
+
addDirtyPaths(state, ["song.scenes"]);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function handleClipRemoved(state, payload) {
|
|
389
|
+
const clipId = payload.clip_id ?? payload.id;
|
|
390
|
+
const clip = clipId ? state.clips[clipId] : null;
|
|
391
|
+
|
|
392
|
+
if (!clip) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const track = state.tracks[clip.trackId];
|
|
397
|
+
|
|
398
|
+
if (track) {
|
|
399
|
+
track.sessionClipIds = track.sessionClipIds.filter((currentClipId) => currentClipId !== clipId);
|
|
400
|
+
track.arrangementClipIds = track.arrangementClipIds.filter(
|
|
401
|
+
(currentClipId) => currentClipId !== clipId
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
delete state.clips[clipId];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function handleDeviceRemoved(state, payload) {
|
|
409
|
+
const deviceId = payload.device_id ?? payload.id;
|
|
410
|
+
|
|
411
|
+
if (!deviceId) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const device = state.devices[deviceId];
|
|
416
|
+
|
|
417
|
+
if (device) {
|
|
418
|
+
const track = state.tracks[device.trackId];
|
|
419
|
+
|
|
420
|
+
if (track) {
|
|
421
|
+
track.deviceIds = track.deviceIds.filter((currentDeviceId) => currentDeviceId !== deviceId);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
removeDevice(state, deviceId);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function handleParameterRemoved(state, payload) {
|
|
429
|
+
const parameterId = payload.parameter_id ?? payload.id;
|
|
430
|
+
|
|
431
|
+
if (!parameterId) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const parameter = state.parameters[parameterId];
|
|
436
|
+
|
|
437
|
+
if (parameter) {
|
|
438
|
+
const device = state.devices[parameter.deviceId];
|
|
439
|
+
|
|
440
|
+
if (device) {
|
|
441
|
+
device.parameterIds = device.parameterIds.filter(
|
|
442
|
+
(currentParameterId) => currentParameterId !== parameterId
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
delete state.parameters[parameterId];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const eventHandlers = {
|
|
451
|
+
"transport.changed": (state, payload, options) => handleTransportChanged(state, payload, options),
|
|
452
|
+
"selection.changed": (state, payload, options) => handleSelectionChanged(state, payload, options),
|
|
453
|
+
"track.added": (state, payload, options) => upsertTrackBundle(state, payload.track ?? payload, options),
|
|
454
|
+
"track.updated": (state, payload, options) => upsertTrackBundle(state, payload.track ?? payload, options),
|
|
455
|
+
"track.removed": (state, payload) => handleTrackRemoved(state, payload),
|
|
456
|
+
"scene.added": (state, payload, options) => upsertScene(state, payload.scene ?? payload, options),
|
|
457
|
+
"scene.updated": (state, payload, options) => upsertScene(state, payload.scene ?? payload, options),
|
|
458
|
+
"scene.removed": (state, payload) => handleSceneRemoved(state, payload),
|
|
459
|
+
"clip.updated": (state, payload, options) => upsertClip(state, payload.clip ?? payload, options),
|
|
460
|
+
"clip.removed": (state, payload) => handleClipRemoved(state, payload),
|
|
461
|
+
"device.updated": (state, payload, options) =>
|
|
462
|
+
upsertDevice(state, payload.device ?? payload, options),
|
|
463
|
+
"device.removed": (state, payload) => handleDeviceRemoved(state, payload),
|
|
464
|
+
"device.selected": (state, payload, options) =>
|
|
465
|
+
handleSelectionChanged(
|
|
466
|
+
state,
|
|
467
|
+
{ ...state.selection, selected_device_id: payload.device_id ?? payload.id ?? null },
|
|
468
|
+
options
|
|
469
|
+
),
|
|
470
|
+
"parameter.updated": (state, payload, options) =>
|
|
471
|
+
upsertParameter(state, payload.parameter ?? payload, options),
|
|
472
|
+
"parameter.removed": (state, payload) => handleParameterRemoved(state, payload),
|
|
473
|
+
"state.dirty": (state, payload) => addDirtyPaths(state, payload.paths ?? []),
|
|
474
|
+
"state.resynced": (state, payload) => clearDirtyPaths(state, payload.paths ?? [])
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
export function applyEvent(state, eventEnvelope, options = {}) {
|
|
478
|
+
const nextState = structuredClone(state);
|
|
479
|
+
const eventName = eventEnvelope.event ?? eventEnvelope.name;
|
|
480
|
+
const payload = eventEnvelope.payload ?? {};
|
|
481
|
+
const observedAt = isoNow(options.observedAt ?? eventEnvelope.observed_at);
|
|
482
|
+
const handler = eventHandlers[eventName];
|
|
483
|
+
|
|
484
|
+
if (handler) {
|
|
485
|
+
handler(nextState, payload, { observedAt });
|
|
486
|
+
} else {
|
|
487
|
+
addDirtyPaths(nextState, payload.paths ?? ["unknown"]);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
updateMetaAfterMutation(nextState, {
|
|
491
|
+
observedAt,
|
|
492
|
+
lastEventAt: observedAt
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
return nextState;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function markDirtyPaths(state, dirtyPaths = [], options = {}) {
|
|
499
|
+
const nextState = structuredClone(state);
|
|
500
|
+
addDirtyPaths(nextState, dirtyPaths);
|
|
501
|
+
updateMetaAfterMutation(nextState, {
|
|
502
|
+
observedAt: options.observedAt
|
|
503
|
+
});
|
|
504
|
+
return nextState;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function reconcileSubtree(state, descriptor, options = {}) {
|
|
508
|
+
const nextState = structuredClone(state);
|
|
509
|
+
const kind = descriptor.kind;
|
|
510
|
+
|
|
511
|
+
if (kind === "track" && descriptor.payload) {
|
|
512
|
+
upsertTrackBundle(nextState, descriptor.payload, options);
|
|
513
|
+
clearDirtyPaths(nextState, [`track:${descriptor.payload.id ?? "unknown"}`]);
|
|
514
|
+
} else if (kind === "scene" && descriptor.payload) {
|
|
515
|
+
upsertScene(nextState, descriptor.payload, options);
|
|
516
|
+
clearDirtyPaths(nextState, ["song.scenes"]);
|
|
517
|
+
} else if (kind === "clip" && descriptor.payload) {
|
|
518
|
+
upsertClip(nextState, descriptor.payload, options);
|
|
519
|
+
} else if (kind === "selection" && descriptor.payload) {
|
|
520
|
+
handleSelectionChanged(nextState, descriptor.payload, options);
|
|
521
|
+
} else if (kind === "transport" && descriptor.payload) {
|
|
522
|
+
handleTransportChanged(nextState, descriptor.payload, options);
|
|
523
|
+
} else if (kind === "snapshot" && descriptor.payload) {
|
|
524
|
+
return applySnapshot(nextState, descriptor.payload, options);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
updateMetaAfterMutation(nextState, {
|
|
528
|
+
observedAt: options.observedAt
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
return nextState;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function createStateEngine(initialState = createInitialState()) {
|
|
535
|
+
let currentState = structuredClone(initialState);
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
getState() {
|
|
539
|
+
return structuredClone(currentState);
|
|
540
|
+
},
|
|
541
|
+
applySnapshot(snapshot, options = {}) {
|
|
542
|
+
currentState = applySnapshot(currentState, snapshot, options);
|
|
543
|
+
return this.getState();
|
|
544
|
+
},
|
|
545
|
+
applyEvent(eventEnvelope, options = {}) {
|
|
546
|
+
currentState = applyEvent(currentState, eventEnvelope, options);
|
|
547
|
+
return this.getState();
|
|
548
|
+
},
|
|
549
|
+
markDirtyPaths(dirtyPaths, options = {}) {
|
|
550
|
+
currentState = markDirtyPaths(currentState, dirtyPaths, options);
|
|
551
|
+
return this.getState();
|
|
552
|
+
},
|
|
553
|
+
reconcileSubtree(descriptor, options = {}) {
|
|
554
|
+
currentState = reconcileSubtree(currentState, descriptor, options);
|
|
555
|
+
return this.getState();
|
|
556
|
+
},
|
|
557
|
+
query: {
|
|
558
|
+
summarizeProject: () => summarizeProject(currentState),
|
|
559
|
+
getSelectedContext: () => getSelectedContext(currentState),
|
|
560
|
+
findTrack: (query) => findTrack(currentState, query),
|
|
561
|
+
getTrackDetails: (trackId) => getTrackDetails(currentState, trackId),
|
|
562
|
+
listPlayingClips: () => listPlayingClips(currentState),
|
|
563
|
+
searchEntities: (query) => searchEntities(currentState, query)
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const trackSectionOrder = {
|
|
2
|
+
visible: 0,
|
|
3
|
+
return: 1,
|
|
4
|
+
master: 2
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function makeTrackId(section = "visible", index = 0) {
|
|
8
|
+
return `track:${section}:${index}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function makeSceneId(index = 0) {
|
|
12
|
+
return `scene:${index}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function makeSessionClipId(trackId, slotIndex = 0) {
|
|
16
|
+
return `clip:session:${trackId}:slot:${slotIndex}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function makeArrangementClipId(trackId, index = 0) {
|
|
20
|
+
return `clip:arrangement:${trackId}:index:${index}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function makeDeviceId(trackId, index = 0) {
|
|
24
|
+
return `device:${trackId}:index:${index}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function makeParameterId(deviceId, index = 0) {
|
|
28
|
+
return `parameter:${deviceId}:param:${index}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function compareTrackIds(leftId, rightId) {
|
|
32
|
+
const left = parseTrackId(leftId);
|
|
33
|
+
const right = parseTrackId(rightId);
|
|
34
|
+
|
|
35
|
+
if (left.section !== right.section) {
|
|
36
|
+
return trackSectionOrder[left.section] - trackSectionOrder[right.section];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return left.index - right.index;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parseTrackId(trackId) {
|
|
43
|
+
const [, section, rawIndex] = trackId.split(":");
|
|
44
|
+
return {
|
|
45
|
+
id: trackId,
|
|
46
|
+
section,
|
|
47
|
+
index: Number(rawIndex)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function parseSceneId(sceneId) {
|
|
52
|
+
const [, rawIndex] = sceneId.split(":");
|
|
53
|
+
return {
|
|
54
|
+
id: sceneId,
|
|
55
|
+
index: Number(rawIndex)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export {
|
|
2
|
+
applyEvent,
|
|
3
|
+
applySnapshot,
|
|
4
|
+
createInitialState,
|
|
5
|
+
createStateEngine,
|
|
6
|
+
markDirtyPaths,
|
|
7
|
+
reconcileSubtree
|
|
8
|
+
} from "./engine.js";
|
|
9
|
+
export {
|
|
10
|
+
compareTrackIds,
|
|
11
|
+
makeArrangementClipId,
|
|
12
|
+
makeDeviceId,
|
|
13
|
+
makeParameterId,
|
|
14
|
+
makeSceneId,
|
|
15
|
+
makeSessionClipId,
|
|
16
|
+
makeTrackId,
|
|
17
|
+
parseSceneId,
|
|
18
|
+
parseTrackId
|
|
19
|
+
} from "./ids.js";
|
|
20
|
+
export {
|
|
21
|
+
normalizeApplication,
|
|
22
|
+
normalizeCapabilities,
|
|
23
|
+
normalizeClip,
|
|
24
|
+
normalizeDevice,
|
|
25
|
+
normalizeParameter,
|
|
26
|
+
normalizeScene,
|
|
27
|
+
normalizeSelection,
|
|
28
|
+
normalizeSong,
|
|
29
|
+
normalizeTrack,
|
|
30
|
+
normalizeTrackBundle
|
|
31
|
+
} from "./normalize.js";
|
|
32
|
+
export {
|
|
33
|
+
findTrack,
|
|
34
|
+
getSelectedContext,
|
|
35
|
+
getTrackDetails,
|
|
36
|
+
listPlayingClips,
|
|
37
|
+
searchEntities,
|
|
38
|
+
summarizeProject
|
|
39
|
+
} from "./queries.js";
|
|
40
|
+
export { loadTraceFile, parseTraceText, replayTrace } from "./replay.js";
|