@opendaw/studio-core 0.0.130 → 0.0.132
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/dist/AssetService.d.ts.map +1 -1
- package/dist/AssetService.js +11 -2
- package/dist/AudioOfflineRenderer.js +1 -1
- package/dist/EffectBox.d.ts +2 -2
- package/dist/EffectBox.d.ts.map +1 -1
- package/dist/EffectFactories.d.ts +3 -0
- package/dist/EffectFactories.d.ts.map +1 -1
- package/dist/EffectFactories.js +19 -3
- package/dist/Engine.d.ts +1 -1
- package/dist/Engine.d.ts.map +1 -1
- package/dist/EngineFacade.d.ts +1 -1
- package/dist/EngineFacade.d.ts.map +1 -1
- package/dist/EngineFacade.js +2 -2
- package/dist/EngineWorklet.d.ts +1 -1
- package/dist/EngineWorklet.d.ts.map +1 -1
- package/dist/EngineWorklet.js +9 -46
- package/dist/MonitoringRouter.d.ts +10 -0
- package/dist/MonitoringRouter.d.ts.map +1 -0
- package/dist/MonitoringRouter.js +89 -0
- package/dist/capture/CaptureAudio.d.ts +10 -0
- package/dist/capture/CaptureAudio.d.ts.map +1 -1
- package/dist/capture/CaptureAudio.js +105 -30
- package/dist/capture/RecordAudio.d.ts.map +1 -1
- package/dist/capture/RecordAudio.js +10 -2
- package/dist/capture/Recording.js +1 -1
- package/dist/processors.js +28 -28
- package/dist/processors.js.map +4 -4
- package/dist/project/AudioWavExport.d.ts +6 -0
- package/dist/project/AudioWavExport.d.ts.map +1 -0
- package/dist/project/AudioWavExport.js +15 -0
- package/dist/project/NoteMidiExport.d.ts +9 -0
- package/dist/project/NoteMidiExport.d.ts.map +1 -0
- package/dist/project/NoteMidiExport.js +27 -0
- package/dist/project/Project.d.ts.map +1 -1
- package/dist/project/Project.js +1 -2
- package/dist/project/ProjectApi.d.ts +3 -1
- package/dist/project/ProjectApi.d.ts.map +1 -1
- package/dist/project/ProjectApi.js +8 -0
- package/dist/project/ProjectStorage.d.ts.map +1 -1
- package/dist/project/ProjectStorage.js +1 -0
- package/dist/project/audio/AudioContentModifier.d.ts.map +1 -1
- package/dist/project/audio/AudioContentModifier.js +43 -3
- package/dist/project/index.d.ts +2 -0
- package/dist/project/index.d.ts.map +1 -1
- package/dist/project/index.js +2 -0
- package/dist/project/migration/MigrateValueEventCollection.test.js +3 -3
- package/dist/samples/OpenSampleAPI.d.ts +1 -0
- package/dist/samples/OpenSampleAPI.d.ts.map +1 -1
- package/dist/samples/OpenSampleAPI.js +1 -0
- package/dist/samples/SampleService.js +1 -1
- package/dist/soundfont/DefaultSoundfontLoader.d.ts.map +1 -1
- package/dist/soundfont/DefaultSoundfontLoader.js +3 -0
- package/dist/soundfont/OpenSoundfontAPI.d.ts +1 -0
- package/dist/soundfont/OpenSoundfontAPI.d.ts.map +1 -1
- package/dist/soundfont/OpenSoundfontAPI.js +1 -0
- package/dist/soundfont/SoundfontService.js +1 -1
- package/dist/sync-log/SyncLogWriter.d.ts.map +1 -1
- package/dist/sync-log/SyncLogWriter.js +3 -2
- package/dist/ui/clipboard/ClipboardUtils.js +1 -1
- package/dist/ui/clipboard/types/DevicesClipboardHandler.d.ts.map +1 -1
- package/dist/ui/clipboard/types/DevicesClipboardHandler.js +22 -12
- package/dist/ui/clipboard/types/DevicesClipboardHandler.test.js +124 -4
- package/dist/ysync/YService.d.ts.map +1 -1
- package/dist/ysync/YService.js +0 -5
- package/dist/ysync/YSync.d.ts +1 -0
- package/dist/ysync/YSync.d.ts.map +1 -1
- package/dist/ysync/YSync.js +127 -84
- package/dist/ysync/YSync.test.d.ts +2 -0
- package/dist/ysync/YSync.test.d.ts.map +1 -0
- package/dist/ysync/YSync.test.js +259 -0
- package/package.json +16 -15
|
@@ -195,9 +195,10 @@ describe("DevicesClipboardHandler", () => {
|
|
|
195
195
|
.map(pointer => pointer.box);
|
|
196
196
|
const mandatoryDeps = Array.from(boxGraph.dependenciesOf(deviceBox, {
|
|
197
197
|
alwaysFollowMandatory: true,
|
|
198
|
+
stopAtResources: true,
|
|
198
199
|
excludeBox: (dep) => dep.ephemeral || DeviceBoxUtils.isDeviceBox(dep)
|
|
199
200
|
|| dep.name === RootBox.ClassName
|
|
200
|
-
}).boxes).filter(dep =>
|
|
201
|
+
}).boxes).filter(dep => dep.resource !== "preserved");
|
|
201
202
|
const preserved = [deviceBox, ...ownedChildren].flatMap(root => Array.from(boxGraph.dependenciesOf(root, {
|
|
202
203
|
alwaysFollowMandatory: true,
|
|
203
204
|
excludeBox: (dep) => dep.ephemeral || DeviceBoxUtils.isDeviceBox(dep)
|
|
@@ -215,6 +216,7 @@ describe("DevicesClipboardHandler", () => {
|
|
|
215
216
|
trackContent.push(regionPointer.box);
|
|
216
217
|
const regionDeps = Array.from(boxGraph.dependenciesOf(regionPointer.box, {
|
|
217
218
|
alwaysFollowMandatory: true,
|
|
219
|
+
stopAtResources: true,
|
|
218
220
|
excludeBox: (dep) => dep.ephemeral
|
|
219
221
|
|| isInstanceOf(dep, TrackBox)
|
|
220
222
|
|| DeviceBoxUtils.isDeviceBox(dep)
|
|
@@ -232,7 +234,7 @@ describe("DevicesClipboardHandler", () => {
|
|
|
232
234
|
return true;
|
|
233
235
|
});
|
|
234
236
|
};
|
|
235
|
-
const makePasteMapper = (targetAudioUnit, replaceInstrument) => ({
|
|
237
|
+
const makePasteMapper = (targetAudioUnit, replaceInstrument, hasInstrument = true) => ({
|
|
236
238
|
mapPointer: (pointer) => {
|
|
237
239
|
if (pointer.pointerType === Pointers.InstrumentHost && replaceInstrument) {
|
|
238
240
|
return Option.wrap(targetAudioUnit.input.address);
|
|
@@ -243,7 +245,7 @@ describe("DevicesClipboardHandler", () => {
|
|
|
243
245
|
if (pointer.pointerType === Pointers.MIDIEffectHost) {
|
|
244
246
|
return Option.wrap(targetAudioUnit.midiEffects.address);
|
|
245
247
|
}
|
|
246
|
-
if (pointer.pointerType === Pointers.TrackCollection
|
|
248
|
+
if (pointer.pointerType === Pointers.TrackCollection) {
|
|
247
249
|
return Option.wrap(targetAudioUnit.tracks.address);
|
|
248
250
|
}
|
|
249
251
|
if (pointer.pointerType === Pointers.Automation && replaceInstrument) {
|
|
@@ -251,7 +253,18 @@ describe("DevicesClipboardHandler", () => {
|
|
|
251
253
|
}
|
|
252
254
|
return Option.None;
|
|
253
255
|
},
|
|
254
|
-
excludeBox: (box) =>
|
|
256
|
+
excludeBox: (box) => {
|
|
257
|
+
if (replaceInstrument) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
if (DeviceBoxUtils.isInstrumentDeviceBox(box)) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
if (isInstanceOf(box, TrackBox)) {
|
|
264
|
+
return hasInstrument;
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
255
268
|
});
|
|
256
269
|
// ─────────────────────────────────────────────────────────
|
|
257
270
|
// Audio effect paste
|
|
@@ -284,6 +297,113 @@ describe("DevicesClipboardHandler", () => {
|
|
|
284
297
|
});
|
|
285
298
|
});
|
|
286
299
|
// ─────────────────────────────────────────────────────────
|
|
300
|
+
// Audio effect with automation: copy scope + paste round-trip
|
|
301
|
+
// ─────────────────────────────────────────────────────────
|
|
302
|
+
describe("audio effect with automation", () => {
|
|
303
|
+
it("includes ValueEventCollectionBox when copying effect with automation events", () => {
|
|
304
|
+
const audioUnit = createAudioUnit(source);
|
|
305
|
+
const effect = addAudioEffect(source, audioUnit, "Comp", 0);
|
|
306
|
+
const autoTrack = addAutomationTrack(source, audioUnit, effect.threshold, 0);
|
|
307
|
+
addValueRegion(source, autoTrack, 0, 960);
|
|
308
|
+
const deps = collectDeviceDependencies(effect, source.boxGraph);
|
|
309
|
+
expect(deps.filter(box => isInstanceOf(box, TrackBox)).length).toBe(1);
|
|
310
|
+
expect(deps.filter(box => isInstanceOf(box, ValueRegionBox)).length).toBe(1);
|
|
311
|
+
expect(deps.filter(box => isInstanceOf(box, ValueEventCollectionBox)).length).toBe(1);
|
|
312
|
+
});
|
|
313
|
+
it("includes mirror regions on different tracks of the same device", () => {
|
|
314
|
+
const audioUnit = createAudioUnit(source);
|
|
315
|
+
const effect = addAudioEffect(source, audioUnit, "Comp", 0);
|
|
316
|
+
const trackA = addAutomationTrack(source, audioUnit, effect.threshold, 0);
|
|
317
|
+
const trackB = addAutomationTrack(source, audioUnit, effect.ratio, 1);
|
|
318
|
+
const regionA = addValueRegion(source, trackA, 0, 960);
|
|
319
|
+
const sharedEvents = regionA.events.targetVertex.unwrap().box;
|
|
320
|
+
let regionB;
|
|
321
|
+
source.boxGraph.beginTransaction();
|
|
322
|
+
regionB = ValueRegionBox.create(source.boxGraph, UUID.generate(), box => {
|
|
323
|
+
box.regions.refer(trackB.regions);
|
|
324
|
+
box.events.refer(sharedEvents.owners);
|
|
325
|
+
box.position.setValue(0);
|
|
326
|
+
box.duration.setValue(960);
|
|
327
|
+
});
|
|
328
|
+
source.boxGraph.endTransaction();
|
|
329
|
+
const deps = collectDeviceDependencies(effect, source.boxGraph);
|
|
330
|
+
const regions = deps.filter(box => isInstanceOf(box, ValueRegionBox));
|
|
331
|
+
expect(regions.length).toBe(2);
|
|
332
|
+
expect(regions).toContain(regionA);
|
|
333
|
+
expect(regions).toContain(regionB);
|
|
334
|
+
expect(deps.filter(box => isInstanceOf(box, ValueEventCollectionBox)).length).toBe(1);
|
|
335
|
+
expect(deps.filter(box => isInstanceOf(box, TrackBox)).length).toBe(2);
|
|
336
|
+
});
|
|
337
|
+
it("does not pull in regions from unrelated devices that mirror the same collection", () => {
|
|
338
|
+
const audioUnitA = createAudioUnit(source, 1);
|
|
339
|
+
const effectA = addAudioEffect(source, audioUnitA, "CompA", 0);
|
|
340
|
+
const trackA = addAutomationTrack(source, audioUnitA, effectA.threshold, 0);
|
|
341
|
+
const regionA = addValueRegion(source, trackA, 0, 960);
|
|
342
|
+
const sharedEvents = regionA.events.targetVertex.unwrap().box;
|
|
343
|
+
const audioUnitB = createAudioUnit(source, 2);
|
|
344
|
+
const effectB = addAudioEffect(source, audioUnitB, "CompB", 0);
|
|
345
|
+
const trackB = addAutomationTrack(source, audioUnitB, effectB.threshold, 0);
|
|
346
|
+
source.boxGraph.beginTransaction();
|
|
347
|
+
const unrelatedRegion = ValueRegionBox.create(source.boxGraph, UUID.generate(), box => {
|
|
348
|
+
box.regions.refer(trackB.regions);
|
|
349
|
+
box.events.refer(sharedEvents.owners);
|
|
350
|
+
box.position.setValue(0);
|
|
351
|
+
box.duration.setValue(960);
|
|
352
|
+
});
|
|
353
|
+
source.boxGraph.endTransaction();
|
|
354
|
+
const deps = collectDeviceDependencies(effectA, source.boxGraph);
|
|
355
|
+
const regions = deps.filter(box => isInstanceOf(box, ValueRegionBox));
|
|
356
|
+
expect(regions).toContain(regionA);
|
|
357
|
+
expect(regions).not.toContain(unrelatedRegion);
|
|
358
|
+
const tracks = deps.filter(box => isInstanceOf(box, TrackBox));
|
|
359
|
+
expect(tracks).toContain(trackA);
|
|
360
|
+
expect(tracks).not.toContain(trackB);
|
|
361
|
+
});
|
|
362
|
+
it("round-trip paste of effect with automation events does not throw", () => {
|
|
363
|
+
const sourceAU = createAudioUnit(source);
|
|
364
|
+
const effect = addAudioEffect(source, sourceAU, "Comp", 0);
|
|
365
|
+
const autoTrack = addAutomationTrack(source, sourceAU, effect.threshold, 0);
|
|
366
|
+
addValueRegion(source, autoTrack, 0, 960);
|
|
367
|
+
const deps = collectDeviceDependencies(effect, source.boxGraph);
|
|
368
|
+
const data = ClipboardUtils.serializeBoxes([effect, ...deps]);
|
|
369
|
+
const targetAU = createAudioUnit(target);
|
|
370
|
+
const editing = new BoxEditing(target.boxGraph);
|
|
371
|
+
expect(() => {
|
|
372
|
+
editing.modify(() => {
|
|
373
|
+
ClipboardUtils.deserializeBoxes(data, target.boxGraph, makePasteMapper(targetAU, false, false));
|
|
374
|
+
});
|
|
375
|
+
}).not.toThrow();
|
|
376
|
+
const pastedEffects = targetAU.audioEffects.pointerHub.incoming()
|
|
377
|
+
.filter(pointer => isInstanceOf(pointer.box, CompressorDeviceBox));
|
|
378
|
+
expect(pastedEffects.length).toBe(1);
|
|
379
|
+
const pastedTracks = targetAU.tracks.pointerHub.filter(Pointers.TrackCollection)
|
|
380
|
+
.filter(pointer => isInstanceOf(pointer.box, TrackBox));
|
|
381
|
+
expect(pastedTracks.length).toBe(1);
|
|
382
|
+
});
|
|
383
|
+
it("pasted automation track targets the pasted device's parameter", () => {
|
|
384
|
+
const sourceAU = createAudioUnit(source);
|
|
385
|
+
const effect = addAudioEffect(source, sourceAU, "Comp", 0);
|
|
386
|
+
const autoTrack = addAutomationTrack(source, sourceAU, effect.threshold, 0);
|
|
387
|
+
addValueRegion(source, autoTrack, 0, 960);
|
|
388
|
+
const deps = collectDeviceDependencies(effect, source.boxGraph);
|
|
389
|
+
const data = ClipboardUtils.serializeBoxes([effect, ...deps]);
|
|
390
|
+
const targetAU = createAudioUnit(target);
|
|
391
|
+
const editing = new BoxEditing(target.boxGraph);
|
|
392
|
+
editing.modify(() => {
|
|
393
|
+
ClipboardUtils.deserializeBoxes(data, target.boxGraph, makePasteMapper(targetAU, false, false));
|
|
394
|
+
});
|
|
395
|
+
const pastedEffect = targetAU.audioEffects.pointerHub.incoming()
|
|
396
|
+
.filter(pointer => isInstanceOf(pointer.box, CompressorDeviceBox))
|
|
397
|
+
.map(pointer => pointer.box)[0];
|
|
398
|
+
const pastedTrack = targetAU.tracks.pointerHub.filter(Pointers.TrackCollection)
|
|
399
|
+
.filter(pointer => isInstanceOf(pointer.box, TrackBox))
|
|
400
|
+
.map(pointer => pointer.box)[0];
|
|
401
|
+
expect(pastedTrack).toBeDefined();
|
|
402
|
+
const targetVertex = pastedTrack.target.targetVertex.unwrap();
|
|
403
|
+
expect(targetVertex.box).toBe(pastedEffect);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
// ─────────────────────────────────────────────────────────
|
|
287
407
|
// Instrument paste
|
|
288
408
|
// ─────────────────────────────────────────────────────────
|
|
289
409
|
describe("paste instrument", () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"YService.d.ts","sourceRoot":"","sources":["../../src/ysync/YService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAyC,MAAM,kBAAkB,CAAA;AAKvF,OAAO,EAAC,OAAO,EAAE,UAAU,EAAmB,MAAM,YAAY,CAAA;AAGhE,OAAO,EAAC,iBAAiB,EAAC,MAAM,aAAa,CAAA;AAI7C,yBAAiB,QAAQ,CAAC;IAMtB,KAAY,UAAU,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,iBAAiB,CAAA;KAAE,CAAA;IAEnE,MAAM,eAAe,GAAU,YAAY,MAAM,CAAC,OAAO,CAAC,EAC3B,KAAK,UAAU,EACf,UAAU,MAAM,KAAG,OAAO,CAAC,UAAU,
|
|
1
|
+
{"version":3,"file":"YService.d.ts","sourceRoot":"","sources":["../../src/ysync/YService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAyC,MAAM,kBAAkB,CAAA;AAKvF,OAAO,EAAC,OAAO,EAAE,UAAU,EAAmB,MAAM,YAAY,CAAA;AAGhE,OAAO,EAAC,iBAAiB,EAAC,MAAM,aAAa,CAAA;AAI7C,yBAAiB,QAAQ,CAAC;IAMtB,KAAY,UAAU,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,iBAAiB,CAAA;KAAE,CAAA;IAEnE,MAAM,eAAe,GAAU,YAAY,MAAM,CAAC,OAAO,CAAC,EAC3B,KAAK,UAAU,EACf,UAAU,MAAM,KAAG,OAAO,CAAC,UAAU,CA0D1E,CAAA;CACJ"}
|
package/dist/ysync/YService.js
CHANGED
|
@@ -46,9 +46,6 @@ export var YService;
|
|
|
46
46
|
conflict: () => project.invalid()
|
|
47
47
|
});
|
|
48
48
|
project.own(sync);
|
|
49
|
-
// TODO Remove this cast at some point
|
|
50
|
-
const editing = project.editing;
|
|
51
|
-
editing.disable();
|
|
52
49
|
return { project, provider };
|
|
53
50
|
}
|
|
54
51
|
else {
|
|
@@ -75,8 +72,6 @@ export var YService;
|
|
|
75
72
|
boxGraph.endTransaction();
|
|
76
73
|
project.follow(userInterfaceBox);
|
|
77
74
|
project.own(sync);
|
|
78
|
-
const editing = project.editing;
|
|
79
|
-
editing.disable();
|
|
80
75
|
return { project, provider };
|
|
81
76
|
}
|
|
82
77
|
};
|
package/dist/ysync/YSync.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type Construct<T> = {
|
|
|
8
8
|
};
|
|
9
9
|
export declare class YSync<T> implements Terminable {
|
|
10
10
|
#private;
|
|
11
|
+
static debugging: boolean;
|
|
11
12
|
/** @internal */
|
|
12
13
|
static isEmpty(doc: Y.Doc): boolean;
|
|
13
14
|
static populateRoom<T>({ boxGraph, boxes }: Construct<T>): Promise<YSync<T>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"YSync.d.ts","sourceRoot":"","sources":["../../src/ysync/YSync.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"YSync.d.ts","sourceRoot":"","sources":["../../src/ysync/YSync.ts"],"names":[],"mappings":"AAAA,OAAO,EAUH,QAAQ,EAER,UAAU,EAIb,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAEH,QAAQ,EAOX,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,CAAC,MAAM,KAAK,CAAA;AAIxB,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IACvB,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IACtB,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IACrB,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;CAC/B,CAAA;AAED,qBAAa,KAAK,CAAC,CAAC,CAAE,YAAW,UAAU;;IACvC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAQ;IAEjC,gBAAgB;IAChB,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,GAAG,OAAO;WAItB,YAAY,CAAC,CAAC,EAAE,EAAC,QAAQ,EAAE,KAAK,EAAC,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;WAcnE,QAAQ,CAAC,CAAC,EAAE,EAAC,QAAQ,EAAE,KAAK,EAAC,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBAqChE,EAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAC,EAAE,SAAS,CAAC,CAAC,CAAC;IASrD,SAAS,IAAI,IAAI;CAiOpB"}
|
package/dist/ysync/YSync.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { asDefined, asInstanceOf, assert, EmptyExec, isUndefined, Option, panic, Terminable, Terminator, UUID } from "@opendaw/lib-std";
|
|
1
|
+
import { asDefined, asInstanceOf, assert, EmptyExec, isInstanceOf, isUndefined, Option, panic, Terminable, Terminator, tryCatch, UUID } from "@opendaw/lib-std";
|
|
2
|
+
import { optimizeUpdates } from "@opendaw/lib-box";
|
|
2
3
|
import { YMapper } from "./YMapper";
|
|
3
4
|
import * as Y from "yjs";
|
|
4
5
|
export class YSync {
|
|
6
|
+
static debugging = false;
|
|
5
7
|
/** @internal */
|
|
6
8
|
static isEmpty(doc) {
|
|
7
9
|
return doc.getMap("boxes").size === 0;
|
|
@@ -62,45 +64,51 @@ export class YSync {
|
|
|
62
64
|
terminate() { this.#terminator.terminate(); }
|
|
63
65
|
#setupYjs() {
|
|
64
66
|
const eventHandler = (events, { origin, local }) => {
|
|
65
|
-
const originLabel = typeof origin === "string" ? origin : "
|
|
67
|
+
const originLabel = typeof origin === "string" ? origin : "Unkown Origin";
|
|
66
68
|
const isOwnOrigin = typeof origin === "string" && origin.startsWith("[openDAW]");
|
|
67
69
|
const isHistoryReplay = typeof origin === "string" && origin.startsWith("[history]");
|
|
68
|
-
console.debug(`got ${events.length} ${local ? "local" : "external"} updates from '${originLabel}', isHistoryReplay: ${isHistoryReplay}`);
|
|
70
|
+
console.debug(`got ${events.length} ${local ? "local" : "external"} updates from '${originLabel}', isHistoryReplay: ${isHistoryReplay}, isOwnOrigin: ${isOwnOrigin}`);
|
|
69
71
|
if (isOwnOrigin || (local && !isHistoryReplay)) {
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
74
|
this.#boxGraph.beginTransaction();
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
const result = tryCatch(() => {
|
|
76
|
+
for (const event of events) {
|
|
77
|
+
const path = this.#normalizePath(event.path);
|
|
78
|
+
const keys = event.changes.keys;
|
|
79
|
+
for (const [key, change] of keys.entries()) {
|
|
80
|
+
if (YSync.debugging) {
|
|
81
|
+
console.debug(`${change.action} on ${path}:${key}`);
|
|
82
|
+
}
|
|
83
|
+
if (change.action === "add") {
|
|
84
|
+
assert(path.length === 0, "'Add' cannot have a path");
|
|
85
|
+
this.#createBox(key);
|
|
86
|
+
}
|
|
87
|
+
else if (change.action === "update") {
|
|
88
|
+
if (path.length === 0) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
assert(path.length >= 2, "Invalid path: must have at least 2 elements (uuid, 'fields').");
|
|
92
|
+
this.#updateValue(path, key);
|
|
93
|
+
}
|
|
94
|
+
else if (change.action === "delete") {
|
|
95
|
+
assert(path.length === 0, "'Delete' cannot have a path");
|
|
96
|
+
this.#deleteBox(key);
|
|
84
97
|
}
|
|
85
|
-
assert(path.length >= 2, "Invalid path: must have at least 2 elements (uuid, 'fields').");
|
|
86
|
-
this.#updateValue(path, key);
|
|
87
|
-
}
|
|
88
|
-
else if (change.action === "delete") {
|
|
89
|
-
assert(path.length === 0, "'Delete' cannot have a path");
|
|
90
|
-
this.#deleteBox(key);
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
|
-
}
|
|
94
|
-
try {
|
|
95
100
|
this.#ignoreUpdates = true;
|
|
96
101
|
this.#boxGraph.endTransaction();
|
|
97
102
|
this.#ignoreUpdates = false;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
});
|
|
104
|
+
if (result.status === "failure") {
|
|
105
|
+
this.#ignoreUpdates = false;
|
|
106
|
+
if (this.#boxGraph.inTransaction()) {
|
|
107
|
+
this.#boxGraph.abortTransaction();
|
|
108
|
+
}
|
|
109
|
+
console.warn(`[YSync] Transaction rejected, rolling back:`, result.error);
|
|
110
|
+
this.#rollbackTransaction(events);
|
|
111
|
+
return;
|
|
104
112
|
}
|
|
105
113
|
const highLevelConflict = this.#conflict.mapOr(check => check(), false);
|
|
106
114
|
if (highLevelConflict) {
|
|
@@ -137,13 +145,9 @@ export class YSync {
|
|
|
137
145
|
return path.slice(prefix.length);
|
|
138
146
|
}
|
|
139
147
|
#updateValue(path, key) {
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
console.debug(`Vertex at '${path}' does not exist. Ignoring.`);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const vertex = vertexOption.unwrap("Could not find field");
|
|
148
|
+
const address = YMapper.pathToAddress(path, key);
|
|
149
|
+
const vertex = this.#boxGraph.findVertex(address)
|
|
150
|
+
.unwrap(`Vertex at '${address.toString()}' does not exist.`);
|
|
147
151
|
const [uuidAsString, fieldsKey, ...fieldKeys] = path;
|
|
148
152
|
const targetMap = YMapper.findMap(this.#boxes
|
|
149
153
|
.get(String(uuidAsString))
|
|
@@ -158,17 +162,11 @@ export class YSync {
|
|
|
158
162
|
});
|
|
159
163
|
}
|
|
160
164
|
#deleteBox(key) {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const box = optBox.unwrap();
|
|
167
|
-
// It is possible that Yjs have swallowed the pointer releases since they were 'inside' the box.
|
|
168
|
-
box.outgoingEdges().forEach(([pointer]) => pointer.defer());
|
|
169
|
-
box.incomingEdges().forEach(pointer => pointer.defer());
|
|
170
|
-
this.#boxGraph.unstageBox(box);
|
|
171
|
-
}
|
|
165
|
+
const box = this.#boxGraph.findBox(UUID.parse(key))
|
|
166
|
+
.unwrap(`Box '${key}' does not exist.`);
|
|
167
|
+
box.outgoingEdges().forEach(([pointer]) => pointer.defer());
|
|
168
|
+
box.incomingEdges().forEach(pointer => pointer.defer());
|
|
169
|
+
this.#boxGraph.unstageBox(box);
|
|
172
170
|
}
|
|
173
171
|
#rollbackTransaction(events) {
|
|
174
172
|
console.debug(`rollback ${events.length} events...`);
|
|
@@ -202,51 +200,96 @@ export class YSync {
|
|
|
202
200
|
#setupOpenDAW() {
|
|
203
201
|
return Terminable.many(this.#boxGraph.subscribeTransaction({
|
|
204
202
|
onBeginTransaction: EmptyExec,
|
|
205
|
-
onEndTransaction: () => {
|
|
206
|
-
|
|
207
|
-
|
|
203
|
+
onEndTransaction: (rolledBack) => {
|
|
204
|
+
const pending = this.#updates.splice(0);
|
|
205
|
+
if (this.#ignoreUpdates || rolledBack) {
|
|
208
206
|
return;
|
|
209
207
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const { address: { fieldKeys }, newValue } = update;
|
|
224
|
-
let field = boxObject.get("fields");
|
|
225
|
-
for (let i = 0; i < fieldKeys.length - 1; i++) {
|
|
226
|
-
field = asDefined(field.get(String(fieldKeys[i])), `No field at '${fieldKeys[i]}'`);
|
|
227
|
-
}
|
|
228
|
-
field.set(String(fieldKeys[fieldKeys.length - 1]), newValue);
|
|
229
|
-
}
|
|
230
|
-
else if (update.type === "pointer") {
|
|
231
|
-
const key = UUID.toString(update.address.uuid);
|
|
232
|
-
const boxObject = asDefined(this.#boxes.get(key), "Could not find box");
|
|
233
|
-
const { address: { fieldKeys }, newAddress } = update;
|
|
234
|
-
let field = boxObject.get("fields");
|
|
235
|
-
for (let i = 0; i < fieldKeys.length - 1; i++) {
|
|
236
|
-
field = asDefined(field.get(String(fieldKeys[i])), `No field at '${fieldKeys[i]}'`);
|
|
237
|
-
}
|
|
238
|
-
field.set(String(fieldKeys[fieldKeys.length - 1]), newAddress.mapOr(address => address.toString(), null));
|
|
239
|
-
}
|
|
240
|
-
else if (update.type === "delete") {
|
|
241
|
-
this.#boxes.delete(UUID.toString(update.uuid));
|
|
242
|
-
}
|
|
243
|
-
}), "[openDAW] updates");
|
|
244
|
-
this.#updates.length = 0;
|
|
208
|
+
const optimized = optimizeUpdates(pending);
|
|
209
|
+
if (optimized.length === 0) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const result = tryCatch(() => this.#getDoc()
|
|
213
|
+
.transact(() => optimized.forEach(update => this.#applyUpdate(update)), "[openDAW] updates"));
|
|
214
|
+
if (result.status === "failure") {
|
|
215
|
+
console.error("[YSync] flush failed, dropping updates", {
|
|
216
|
+
count: optimized.length,
|
|
217
|
+
error: result.error
|
|
218
|
+
});
|
|
219
|
+
throw result.error;
|
|
220
|
+
}
|
|
245
221
|
}
|
|
246
222
|
}), this.#boxGraph.subscribeToAllUpdatesImmediate({
|
|
247
223
|
onUpdate: (update) => this.#updates.push(update)
|
|
248
224
|
}));
|
|
249
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* TRANSFER ONE CHANGE FROM OPENDAW TO YJS
|
|
228
|
+
*/
|
|
229
|
+
#applyUpdate(update) {
|
|
230
|
+
if (update.type === "new") {
|
|
231
|
+
const uuid = update.uuid;
|
|
232
|
+
const key = UUID.toString(uuid);
|
|
233
|
+
const optBox = this.#boxGraph.findBox(uuid);
|
|
234
|
+
if (optBox.isEmpty()) {
|
|
235
|
+
// Phantom box: created and removed in same transaction.
|
|
236
|
+
// optimizeUpdates should have filtered this, but guard in case.
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.#boxes.set(key, YMapper.createBoxMap(optBox.unwrap()));
|
|
240
|
+
}
|
|
241
|
+
else if (update.type === "primitive") {
|
|
242
|
+
const key = UUID.toString(update.address.uuid);
|
|
243
|
+
const boxObject = this.#boxes.get(key);
|
|
244
|
+
if (!isInstanceOf(boxObject, Y.Map)) {
|
|
245
|
+
console.warn(`[YSync] primitive update skipped: box '${key}' missing`);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const field = this.#resolveFieldMap(boxObject, key, update.address.fieldKeys);
|
|
249
|
+
if (field === undefined) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
field.set(String(update.address.fieldKeys[update.address.fieldKeys.length - 1]), update.newValue);
|
|
253
|
+
}
|
|
254
|
+
else if (update.type === "pointer") {
|
|
255
|
+
const key = UUID.toString(update.address.uuid);
|
|
256
|
+
const boxObject = this.#boxes.get(key);
|
|
257
|
+
if (!isInstanceOf(boxObject, Y.Map)) {
|
|
258
|
+
console.warn(`[YSync] pointer update skipped: box '${key}' missing`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const field = this.#resolveFieldMap(boxObject, key, update.address.fieldKeys);
|
|
262
|
+
if (field === undefined) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
field.set(String(update.address.fieldKeys[update.address.fieldKeys.length - 1]), update.newAddress.mapOr(address => address.toString(), null));
|
|
266
|
+
}
|
|
267
|
+
else if (update.type === "delete") {
|
|
268
|
+
this.#boxes.delete(UUID.toString(update.uuid));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Walks from the box map down to the Y.Map that owns `fieldKeys[last]`.
|
|
273
|
+
* Returns `undefined` (with a warning) instead of throwing if the path
|
|
274
|
+
* cannot be resolved — protects the yjs transaction from partial writes.
|
|
275
|
+
*/
|
|
276
|
+
#resolveFieldMap(boxObject, key, fieldKeys) {
|
|
277
|
+
const fieldsValue = boxObject.get("fields");
|
|
278
|
+
if (!isInstanceOf(fieldsValue, Y.Map)) {
|
|
279
|
+
console.warn(`[YSync] box '${key}' missing 'fields' Y.Map; skipping update`);
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
let field = fieldsValue;
|
|
283
|
+
for (let i = 0; i < fieldKeys.length - 1; i++) {
|
|
284
|
+
const next = field.get(String(fieldKeys[i]));
|
|
285
|
+
if (!isInstanceOf(next, Y.Map)) {
|
|
286
|
+
console.warn(`[YSync] box '${key}' field path broken at '${fieldKeys[i]}'; skipping update`);
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
field = next;
|
|
290
|
+
}
|
|
291
|
+
return field;
|
|
292
|
+
}
|
|
250
293
|
#getDoc() {
|
|
251
294
|
return asDefined(this.#boxes.doc, "Y.Map is not connect to Y.Doc");
|
|
252
295
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"YSync.test.d.ts","sourceRoot":"","sources":["../../src/ysync/YSync.test.ts"],"names":[],"mappings":""}
|