sygnal 7.0.0 → 7.2.0-preview.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/CHANGELOG.md +632 -0
- package/dist/agent-ui.cjs.js +592 -0
- package/dist/agent-ui.cjs.js.map +1 -0
- package/dist/agent-ui.esm.js +582 -0
- package/dist/agent-ui.esm.js.map +1 -0
- package/dist/api.cjs.js +63 -0
- package/dist/api.cjs.js.map +1 -0
- package/dist/api.esm.js +2 -0
- package/dist/api.esm.js.map +1 -0
- package/dist/astro/index.cjs.js +968 -4
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +968 -4
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js +73 -45
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +73 -45
- package/dist/astro/server.mjs.map +1 -1
- package/dist/canvas.cjs.js +2919 -0
- package/dist/canvas.cjs.js.map +1 -0
- package/dist/canvas.esm.js +2866 -0
- package/dist/canvas.esm.js.map +1 -0
- package/dist/devtools.cjs.js +38 -6
- package/dist/devtools.cjs.js.map +1 -1
- package/dist/devtools.esm.js +38 -6
- package/dist/devtools.esm.js.map +1 -1
- package/dist/diagnostics.cjs.js +224 -20
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +224 -20
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/accessibility.md +1 -1
- package/dist/guide/ai-compose.md +150 -0
- package/dist/guide/behaviors.md +2 -1
- package/dist/guide/collaborative-state.md +265 -0
- package/dist/guide/element-commands.md +36 -2
- package/dist/guide/forms-reference.md +5 -5
- package/dist/guide/forms.md +3 -2
- package/dist/guide/http.md +28 -1
- package/dist/guide/islands.md +240 -0
- package/dist/guide/recipes/code-editor.md +1 -1
- package/dist/guide/renderers/agent-ui.md +138 -0
- package/dist/guide/renderers/custom-targets.md +126 -0
- package/dist/guide/renderers/overview.md +120 -0
- package/dist/guide/renderers/react-native.md +128 -0
- package/dist/guide/renderers/terminal.md +306 -0
- package/dist/guide/renderers/three.md +332 -0
- package/dist/guide/renderers/write-once.md +137 -0
- package/dist/guide/resources.md +2 -0
- package/dist/guide/sandboxed-components.md +223 -0
- package/dist/guide/timers.md +32 -5
- package/dist/guide/typed-apis.md +442 -0
- package/dist/guide/ui/accordion.md +11 -0
- package/dist/guide/ui/combobox.md +112 -8
- package/dist/guide/ui/dialog.md +96 -20
- package/dist/guide/ui/menu.md +57 -5
- package/dist/guide/ui/overview.md +8 -4
- package/dist/guide/ui/popover.md +69 -9
- package/dist/guide/ui/select.md +54 -4
- package/dist/guide/ui/tabs.md +1 -1
- package/dist/guide/ui/tooltip.md +7 -5
- package/dist/guide/virtual-collections.md +3 -0
- package/dist/guide/widgets.md +90 -1
- package/dist/host.cjs.js +2181 -0
- package/dist/host.cjs.js.map +1 -0
- package/dist/host.esm.js +2146 -0
- package/dist/host.esm.js.map +1 -0
- package/dist/index.cjs.js +3948 -586
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +584 -44
- package/dist/index.esm.js +3918 -586
- package/dist/index.esm.js.map +1 -1
- package/dist/primitives.cjs.js +143 -0
- package/dist/primitives.cjs.js.map +1 -0
- package/dist/primitives.esm.js +133 -0
- package/dist/primitives.esm.js.map +1 -0
- package/dist/react-native.cjs.js +248 -0
- package/dist/react-native.cjs.js.map +1 -0
- package/dist/react-native.esm.js +244 -0
- package/dist/react-native.esm.js.map +1 -0
- package/dist/replay.cjs.js +39 -0
- package/dist/replay.cjs.js.map +1 -0
- package/dist/replay.esm.js +2 -0
- package/dist/replay.esm.js.map +1 -0
- package/dist/sandbox-build.cjs.js +140 -0
- package/dist/sandbox-build.cjs.js.map +1 -0
- package/dist/sandbox-build.esm.js +137 -0
- package/dist/sandbox-build.esm.js.map +1 -0
- package/dist/sandbox-kernel.esm.js +212 -0
- package/dist/sandbox-kernel.esm.js.map +1 -0
- package/dist/sandbox-worker.cjs.js +857 -0
- package/dist/sandbox-worker.cjs.js.map +1 -0
- package/dist/sandbox-worker.esm.js +854 -0
- package/dist/sandbox-worker.esm.js.map +1 -0
- package/dist/sandbox.cjs.js +1425 -0
- package/dist/sandbox.cjs.js.map +1 -0
- package/dist/sandbox.esm.js +1406 -0
- package/dist/sandbox.esm.js.map +1 -0
- package/dist/sygnal.min.js +1 -1
- package/dist/sygnal.min.js.map +1 -1
- package/dist/terminal.cjs.js +1314 -0
- package/dist/terminal.cjs.js.map +1 -0
- package/dist/terminal.esm.js +1304 -0
- package/dist/terminal.esm.js.map +1 -0
- package/dist/three.cjs.js +1111 -0
- package/dist/three.cjs.js.map +1 -0
- package/dist/three.esm.js +1079 -0
- package/dist/three.esm.js.map +1 -0
- package/dist/ui-combobox.cjs.js +256 -18
- package/dist/ui-combobox.cjs.js.map +1 -1
- package/dist/ui-combobox.esm.js +256 -18
- package/dist/ui-combobox.esm.js.map +1 -1
- package/dist/ui-menu.cjs.js +150 -16
- package/dist/ui-menu.cjs.js.map +1 -1
- package/dist/ui-menu.esm.js +151 -17
- package/dist/ui-menu.esm.js.map +1 -1
- package/dist/ui-select.cjs.js +95 -9
- package/dist/ui-select.cjs.js.map +1 -1
- package/dist/ui-select.esm.js +95 -9
- package/dist/ui-select.esm.js.map +1 -1
- package/dist/ui.cjs.js +679 -173
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.esm.js +679 -175
- package/dist/ui.esm.js.map +1 -1
- package/dist/vike/config/package.json +1 -1
- package/dist/vite/plugin.cjs.js +968 -4
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +968 -4
- package/dist/vite/plugin.mjs.map +1 -1
- package/dist/worker-page.cjs.js +669 -0
- package/dist/worker-page.cjs.js.map +1 -0
- package/dist/worker-page.esm.js +665 -0
- package/dist/worker-page.esm.js.map +1 -0
- package/dist/worker.cjs.js +377 -0
- package/dist/worker.cjs.js.map +1 -0
- package/dist/worker.esm.js +374 -0
- package/dist/worker.esm.js.map +1 -0
- package/dist/yjs-testing.cjs.js +98 -0
- package/dist/yjs-testing.cjs.js.map +1 -0
- package/dist/yjs-testing.esm.js +77 -0
- package/dist/yjs-testing.esm.js.map +1 -0
- package/dist/yjs.cjs.js +923 -0
- package/dist/yjs.cjs.js.map +1 -0
- package/dist/yjs.esm.js +900 -0
- package/dist/yjs.esm.js.map +1 -0
- package/dist/zag.cjs.js +11 -1
- package/dist/zag.cjs.js.map +1 -1
- package/dist/zag.esm.js +11 -1
- package/dist/zag.esm.js.map +1 -1
- package/llms/ai.md +65 -0
- package/llms/collaboration.md +5 -0
- package/llms/graphics.md +10 -0
- package/llms/renderers.md +7 -0
- package/llms/replay.md +5 -0
- package/llms/sandbox.md +5 -0
- package/llms/typed-apis.md +5 -0
- package/llms/ui.md +10 -0
- package/llms-full.txt +444 -0
- package/llms.txt +37 -35
- package/package.json +139 -3
- package/src/agent-ui/mcp.ts +152 -0
- package/src/agent-ui/snapshot.ts +212 -0
- package/src/agent-ui.d.ts +106 -0
- package/src/agent-ui.ts +249 -0
- package/src/ai.d.ts +95 -2
- package/src/api.d.ts +303 -0
- package/src/api.ts +9 -0
- package/src/canvas/Canvas.ts +585 -0
- package/src/canvas/backend.ts +627 -0
- package/src/canvas/geometry.ts +187 -0
- package/src/canvas/gesture.ts +23 -0
- package/src/canvas/image.ts +197 -0
- package/src/canvas/kit.ts +130 -0
- package/src/canvas/paint.ts +260 -0
- package/src/canvas/pointer.ts +25 -0
- package/src/canvas/snap.ts +68 -0
- package/src/canvas/svg.ts +99 -0
- package/src/canvas.d.ts +373 -0
- package/src/canvas.ts +46 -0
- package/src/core/actions.ts +14 -3
- package/src/core/cell.ts +12 -4
- package/src/core/debug.ts +8 -3
- package/src/core/hooks.ts +16 -2
- package/src/core/hosts/collection.ts +1 -1
- package/src/core/hosts/switchable.ts +26 -8
- package/src/core/instance.ts +98 -45
- package/src/core/markers/portal.ts +4 -0
- package/src/core/registry.ts +4 -0
- package/src/core/runtime.ts +34 -25
- package/src/core/statics.ts +5 -5
- package/src/cycle/dom/EventDelegator.ts +37 -33
- package/src/cycle/dom/MainDOMSource.ts +16 -14
- package/src/cycle/dom/enrichEventStream.ts +13 -9
- package/src/cycle/dom/fromEvent.ts +6 -10
- package/src/cycle/dom/index.ts +11 -7
- package/src/cycle/dom/isolate.ts +13 -6
- package/src/cycle/dom/makeDOMDriver.ts +6 -2
- package/src/cycle/dom/utils.ts +9 -2
- package/src/cycle/run/adapt.ts +4 -12
- package/src/devtools.d.ts +9 -0
- package/src/devtools.ts +2 -1
- package/src/extra/ai/compose/catalog.ts +2 -1
- package/src/extra/ai/compose/decisions.ts +1 -2
- package/src/extra/ai/compose/generated.ts +234 -0
- package/src/extra/ai/compose/index.ts +6 -2
- package/src/extra/ai/compose/layout.ts +3 -1
- package/src/extra/ai/compose/llm.ts +7 -3
- package/src/extra/ai/compose/types.ts +2 -2
- package/src/extra/ai/transports/shared.ts +3 -1
- package/src/extra/api/contract.ts +485 -0
- package/src/extra/api/index.ts +26 -0
- package/src/extra/behaviors.ts +3 -3
- package/src/extra/controlled.ts +44 -0
- package/src/extra/copyAsTest.ts +22 -0
- package/src/extra/devtools.ts +5 -3
- package/src/extra/devtoolsActions.ts +8 -0
- package/src/extra/devtoolsNext.ts +1 -1
- package/src/extra/diagnostics/checks/actionLog.ts +8 -3
- package/src/extra/diagnostics/checks/behaviors.ts +25 -0
- package/src/extra/diagnostics/checks/elementCommands.ts +17 -5
- package/src/extra/diagnostics/checks/index.ts +1 -1
- package/src/extra/diagnostics/checks/next.ts +78 -4
- package/src/extra/diagnostics/checks/props.ts +6 -1
- package/src/extra/diagnostics/checks/replies.ts +11 -7
- package/src/extra/diagnostics/checks/timers.ts +8 -2
- package/src/extra/diagnostics/checks/virtual.ts +11 -0
- package/src/extra/diagnostics/codes.ts +51 -1
- package/src/extra/elementCommands.ts +20 -4
- package/src/extra/fetchDriver.ts +4 -1
- package/src/extra/fetchEventSource.ts +6 -2
- package/src/extra/field.ts +29 -0
- package/src/extra/form.ts +42 -16
- package/src/extra/frame.ts +54 -0
- package/src/extra/keymap.ts +41 -0
- package/src/extra/pager.ts +2 -0
- package/src/extra/persist.ts +2 -1
- package/src/extra/pointer.ts +262 -0
- package/src/extra/processForm.ts +34 -1
- package/src/extra/replay/address.ts +49 -0
- package/src/extra/replay/entropy.ts +99 -0
- package/src/extra/replay/format.ts +263 -0
- package/src/extra/replay/index.ts +8 -0
- package/src/extra/replay/inert.ts +22 -0
- package/src/extra/replay/recorder.ts +247 -0
- package/src/extra/replay/replayer.ts +234 -0
- package/src/extra/replay/report.ts +126 -0
- package/src/extra/replay/schema.ts +52 -0
- package/src/extra/replay/version.ts +3 -0
- package/src/extra/router.ts +7 -3
- package/src/extra/selection.ts +2 -0
- package/src/extra/socketDriver.ts +51 -6
- package/src/extra/ssr.ts +128 -42
- package/src/extra/testing.ts +295 -39
- package/src/extra/timers.ts +96 -7
- package/src/extra/undo.ts +2 -0
- package/src/extra/virtual.ts +25 -2
- package/src/extra/widget.ts +106 -17
- package/src/host/actions.ts +383 -0
- package/src/host/delegation.ts +101 -0
- package/src/host/dom.ts +553 -0
- package/src/host/driver.ts +185 -0
- package/src/host/selector.ts +119 -0
- package/src/host/style.ts +51 -0
- package/src/host.d.ts +298 -0
- package/src/host.ts +46 -0
- package/src/index.d.ts +302 -38
- package/src/index.ts +15 -3
- package/src/pragma/blocks.ts +211 -0
- package/src/pragma/index.ts +44 -10
- package/src/primitives.d.ts +54 -0
- package/src/primitives.ts +136 -0
- package/src/react-native.d.ts +57 -0
- package/src/react-native.ts +285 -0
- package/src/react-peers.d.ts +21 -3
- package/src/replay.d.ts +185 -0
- package/src/replay.ts +6 -0
- package/src/sandbox/build.ts +119 -0
- package/src/sandbox/create.ts +187 -0
- package/src/sandbox/engine.ts +267 -0
- package/src/sandbox/generated.ts +195 -0
- package/src/sandbox/kernel.ts +180 -0
- package/src/sandbox/mirror.ts +269 -0
- package/src/sandbox/protocol.ts +128 -0
- package/src/sandbox/quickjs.ts +74 -0
- package/src/sandbox/ssr.ts +41 -0
- package/src/sandbox/widget.ts +60 -0
- package/src/sandbox/worker-entry.ts +59 -0
- package/src/sandbox/worker.ts +137 -0
- package/src/sandbox-build.d.ts +12 -0
- package/src/sandbox-build.ts +6 -0
- package/src/sandbox.d.ts +299 -0
- package/src/sandbox.ts +25 -0
- package/src/shared.ts +12 -0
- package/src/terminal/paint.ts +712 -0
- package/src/terminal.d.ts +167 -0
- package/src/terminal.ts +530 -0
- package/src/three/assets.ts +170 -0
- package/src/three/host.ts +36 -0
- package/src/three/scene.ts +217 -0
- package/src/three/target.ts +252 -0
- package/src/three.d.ts +221 -0
- package/src/three.ts +30 -0
- package/src/ui/accordion.ts +21 -7
- package/src/ui/dialog.ts +192 -90
- package/src/ui/disclosure.ts +2 -0
- package/src/ui/dom.ts +30 -0
- package/src/ui/layers.ts +132 -0
- package/src/ui/popover.ts +120 -21
- package/src/ui/shared.ts +98 -12
- package/src/ui/stack.ts +18 -0
- package/src/ui/tabs.ts +2 -0
- package/src/ui/toaster.ts +16 -15
- package/src/ui/tooltip.ts +24 -8
- package/src/ui/tooltips.ts +26 -14
- package/src/ui/zag/combobox.ts +180 -13
- package/src/ui/zag/menu.ts +97 -17
- package/src/ui/zag/select.ts +15 -4
- package/src/ui/zag/shared.ts +60 -8
- package/src/ui-combobox.d.ts +16 -1
- package/src/ui-menu.d.ts +22 -7
- package/src/ui-zag-types.d.ts +13 -5
- package/src/ui.d.ts +62 -7
- package/src/ui.ts +2 -2
- package/src/vite/api.ts +186 -0
- package/src/vite/compile.ts +280 -0
- package/src/vite/plugin.d.ts +34 -0
- package/src/vite/plugin.ts +78 -4
- package/src/vite/sandbox.ts +16 -0
- package/src/worker/page.ts +396 -0
- package/src/worker/protocol.ts +48 -0
- package/src/worker/target.ts +282 -0
- package/src/worker-page.d.ts +42 -0
- package/src/worker-page.ts +12 -0
- package/src/worker.d.ts +65 -0
- package/src/worker.ts +16 -0
- package/src/yjs/driver.ts +253 -0
- package/src/yjs/mapping.ts +389 -0
- package/src/yjs/shared.ts +138 -0
- package/src/yjs/testing.ts +91 -0
- package/src/yjs-testing.d.ts +34 -0
- package/src/yjs-testing.ts +7 -0
- package/src/yjs.d.ts +109 -0
- package/src/yjs.ts +18 -0
- package/src/zag.d.ts +2 -0
- package/src/zag.ts +8 -1
package/dist/yjs.cjs.js
ADDED
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var Y = require('yjs');
|
|
4
|
+
var sygnal = require('sygnal');
|
|
5
|
+
var fractionalIndexing = require('fractional-indexing');
|
|
6
|
+
|
|
7
|
+
function _interopNamespaceDefault(e) {
|
|
8
|
+
var n = Object.create(null);
|
|
9
|
+
if (e) {
|
|
10
|
+
Object.keys(e).forEach(function (k) {
|
|
11
|
+
if (k !== 'default') {
|
|
12
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
13
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
get: function () { return e[k]; }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
n.default = e;
|
|
21
|
+
return Object.freeze(n);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var Y__namespace = /*#__PURE__*/_interopNamespaceDefault(Y);
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* 'sygnal/yjs' (research: dev-plans/research/crdt-state): the mapping between a plain JSON state
|
|
28
|
+
* slice and a Yjs document.
|
|
29
|
+
*
|
|
30
|
+
* Inferred from the value unless a `shape` declares it (shared({ shape }), D: Q1):
|
|
31
|
+
* - a plain object -> Y.Map (one LWW register per key)
|
|
32
|
+
* - an array of objects that all have `id` -> a *collection*: a Y.Map keyed by String(id), each item a
|
|
33
|
+
* Y.Map of its fields plus `$o`, a fractional order key (`fractional-indexing`); the array is the
|
|
34
|
+
* items sorted by ($o, id). Concurrent add / edit / move / delete of different items merge; a move
|
|
35
|
+
* is one register write, so it never duplicates or drops an item, and an edit to a moved item
|
|
36
|
+
* survives the move. An empty array is ambiguous: a collection unless the field already holds a
|
|
37
|
+
* list (or the shape says 'list'); without a shape the driver warns once (SYG261, dev).
|
|
38
|
+
* - any other array -> Y.Array of JSON values (spliced by common prefix / suffix)
|
|
39
|
+
* - a string field named in `text` / 'text' in the shape -> Y.Text (character-level merge)
|
|
40
|
+
* - anything else -> a JSON value in its parent (LWW)
|
|
41
|
+
*
|
|
42
|
+
* write(y, prev, next) turns the *local* change prev -> next into Yjs operations. It is a 3-way
|
|
43
|
+
* write: keys / items the local change didn't touch are left alone even when Yjs holds newer remote
|
|
44
|
+
* values for them, and every write is idempotent against the document's current content (a value
|
|
45
|
+
* Yjs already holds isn't written again), so a prev older than the document never reverts or
|
|
46
|
+
* duplicates a remote change. Items that are the same object as before are skipped without a
|
|
47
|
+
* lookup (index identity first).
|
|
48
|
+
*
|
|
49
|
+
* read(y, path, prev, dirty) reads the slice; with `dirty` (the types a transaction changed) it
|
|
50
|
+
* re-reads only those and keeps prev's objects everywhere else (incremental read).
|
|
51
|
+
*/
|
|
52
|
+
const isPlain = (v) => v !== null && typeof v == 'object' && Object.getPrototypeOf(v) === Object.prototype;
|
|
53
|
+
const hasId = (v) => isPlain(v) && (typeof v.id == 'string' || typeof v.id == 'number');
|
|
54
|
+
const isCollMap = (y) => y instanceof Y__namespace.Map && y.get('$c') === 1;
|
|
55
|
+
const same = (a, b) => a === b || JSON.stringify(a) === JSON.stringify(b);
|
|
56
|
+
// ---- shapes ------------------------------------------------------------------------------------
|
|
57
|
+
const COLL = Symbol.for('sygnal.yjs.collection');
|
|
58
|
+
const KINDS = { text: 't', list: 'l', collection: 'c', map: 'm', value: 'v' };
|
|
59
|
+
/** shape: { items: collection({ text: 'text' }) }: an id-keyed collection whose items have these field shapes */
|
|
60
|
+
const collection = (item = {}) => ({ [COLL]: item });
|
|
61
|
+
/** a public shape ({ title: 'text', items: collection(...), tags: 'list' }) as nodes */
|
|
62
|
+
const normShape = (s) => {
|
|
63
|
+
if (s == null)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (typeof s == 'string') {
|
|
66
|
+
if (!KINDS[s])
|
|
67
|
+
throw new TypeError(`shared(): unknown shape '${s}'; use 'text', 'list', 'collection', 'map', 'value', collection({ ... }) or an object of fields`);
|
|
68
|
+
return { k: KINDS[s] };
|
|
69
|
+
}
|
|
70
|
+
if (s[COLL])
|
|
71
|
+
return { k: 'c', i: fields(s[COLL]) };
|
|
72
|
+
if (isPlain(s))
|
|
73
|
+
return { k: 'm', f: fields(s) };
|
|
74
|
+
throw new TypeError('shared(): a shape is a string, collection({ ... }) or an object of fields');
|
|
75
|
+
};
|
|
76
|
+
const fields = (o) => {
|
|
77
|
+
const f = {};
|
|
78
|
+
for (const k in o)
|
|
79
|
+
f[k] = normShape(o[k]);
|
|
80
|
+
return f;
|
|
81
|
+
};
|
|
82
|
+
// the item shape of a collection field as a map node (its fields)
|
|
83
|
+
const itemNode = (sh) => sh?.i ? { k: 'm', f: sh.i } : undefined;
|
|
84
|
+
// ---- fractional order keys -----------------------------------------------------------------------
|
|
85
|
+
// a key generateKeyBetween accepts (its default base-62 format: a head letter giving the integer
|
|
86
|
+
// part's length, 'a' 2 .. 'z' 27 and 'Z' 2 .. 'A' 27, then digits; no fraction ending in '0'); any
|
|
87
|
+
// other value (an older document's key) is rekeyed
|
|
88
|
+
const B62 = /^[0-9A-Za-z]+$/;
|
|
89
|
+
const valid = (k) => {
|
|
90
|
+
if (typeof k != 'string' || !B62.test(k))
|
|
91
|
+
return false;
|
|
92
|
+
const c = k.charCodeAt(0), n = c >= 97 ? c - 95 : c <= 90 && c >= 65 ? 92 - c : 0;
|
|
93
|
+
return n > 0 && k.length >= n && (k.length == n || k[k.length - 1] != '0') && !/^A0{26}/.test(k);
|
|
94
|
+
};
|
|
95
|
+
const splice = (a, b, eq) => {
|
|
96
|
+
let i = 0, j = 0;
|
|
97
|
+
while (i < a.length && i < b.length && eq(a[i], b[i]))
|
|
98
|
+
i++;
|
|
99
|
+
while (j < a.length - i && j < b.length - i && eq(a[a.length - 1 - j], b[b.length - 1 - j]))
|
|
100
|
+
j++;
|
|
101
|
+
return { i, del: a.length - i - j, ins: b.slice(i, b.length - j) };
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* The local splice prev -> next, moved onto a document whose content `cur` differs from prev by a
|
|
105
|
+
* remote splice: shifted when it lies after the remote one, kept when before; an overlapping edit
|
|
106
|
+
* is clamped (approximate; exact when the document matches either side).
|
|
107
|
+
*/
|
|
108
|
+
const rebase = (l, prev, cur, eq) => {
|
|
109
|
+
const r = splice(prev, cur, eq);
|
|
110
|
+
if (l.i + l.del <= r.i)
|
|
111
|
+
return l;
|
|
112
|
+
if (l.i >= r.i + r.del)
|
|
113
|
+
return { ...l, i: l.i + r.ins.length - r.del };
|
|
114
|
+
const i = Math.min(l.i, r.i);
|
|
115
|
+
return { i, del: Math.max(0, Math.min(l.del, cur.length - i)), ins: l.ins };
|
|
116
|
+
};
|
|
117
|
+
// eq compares elements (characters, list values)
|
|
118
|
+
const spliceInto = (cur, prev, next, eq, apply) => {
|
|
119
|
+
const all = (a, b) => a === b || a.length === b.length && splice(a, b, eq).del === 0;
|
|
120
|
+
if (all(cur, next))
|
|
121
|
+
return; // already there (a remote change, or no change)
|
|
122
|
+
const l = splice(prev, next, eq);
|
|
123
|
+
apply(all(cur, prev) ? l : rebase(l, prev, cur, eq));
|
|
124
|
+
};
|
|
125
|
+
const strEq = (a, b) => a === b;
|
|
126
|
+
const kindOf = (k, n, cur, o, sh, name) => {
|
|
127
|
+
const d = sh?.k;
|
|
128
|
+
if (d == 't' && typeof n == 'string' || d == 'l' && Array.isArray(n) || d == 'm' && isPlain(n) || d == 'v')
|
|
129
|
+
return d;
|
|
130
|
+
if (d == 'c' && Array.isArray(n) && n.every(hasId))
|
|
131
|
+
return 'c';
|
|
132
|
+
if (typeof n == 'string' && o.text?.includes(k))
|
|
133
|
+
return 't';
|
|
134
|
+
if (Array.isArray(n) && n.every(hasId)) {
|
|
135
|
+
if (n.length)
|
|
136
|
+
return 'c';
|
|
137
|
+
if (cur == 'l' || cur == 'c')
|
|
138
|
+
return cur;
|
|
139
|
+
// an empty array the shape doesn't name: a guess (a collection); a list stays a list
|
|
140
|
+
if (!d)
|
|
141
|
+
o.ambiguous?.(name);
|
|
142
|
+
return 'c';
|
|
143
|
+
}
|
|
144
|
+
return isPlain(n) ? 'm' : Array.isArray(n) ? 'l' : 'v';
|
|
145
|
+
};
|
|
146
|
+
const nestedKind = (y) => y instanceof Y__namespace.Text ? 't' : isCollMap(y) ? 'c' : y instanceof Y__namespace.Map ? 'm' : y instanceof Y__namespace.Array ? 'l' : undefined;
|
|
147
|
+
const rootOf = (doc, name, k) => k == 't' ? doc.getText(name + '#t') : k == 'l' ? doc.getArray(name + '#l') : doc.getMap(name + '#' + k);
|
|
148
|
+
const clear = (t) => {
|
|
149
|
+
if (t instanceof Y__namespace.Map)
|
|
150
|
+
for (const k of [...t.keys()])
|
|
151
|
+
t.delete(k);
|
|
152
|
+
else if (t.length)
|
|
153
|
+
t.delete(0, t.length);
|
|
154
|
+
};
|
|
155
|
+
const marker = (v) => isPlain(v) && typeof v.$y == 'string' ? v.$y : undefined;
|
|
156
|
+
/** the local change prev -> next of a map (the slice: writeMap(doc.getMap(key), last, value, o, key, shape)) */
|
|
157
|
+
const writeMap = (y, prev, next, o, path, sh, label = path || '') => {
|
|
158
|
+
prev = isPlain(prev) ? prev : {};
|
|
159
|
+
for (const k in prev)
|
|
160
|
+
if (!(k in next) && y.has(k)) {
|
|
161
|
+
const m = path != null && marker(y.get(k));
|
|
162
|
+
if (m)
|
|
163
|
+
clear(rootOf(y.doc, path + '.' + k, m));
|
|
164
|
+
y.delete(k);
|
|
165
|
+
}
|
|
166
|
+
for (const k in next)
|
|
167
|
+
if (!(k in prev) || prev[k] !== next[k] || !y.has(k))
|
|
168
|
+
writeValue(y, k, prev[k], next[k], o, path, sh?.f?.[k], label + '.' + k);
|
|
169
|
+
};
|
|
170
|
+
const writeValue = (y, k, p, n, o, path, sh, label) => {
|
|
171
|
+
const cur = y.get(k), name = path != null ? path + '.' + k : undefined;
|
|
172
|
+
const had = name != null ? marker(cur) : nestedKind(cur), want = kindOf(k, n, had, o, sh, label);
|
|
173
|
+
let t;
|
|
174
|
+
if (name != null) {
|
|
175
|
+
if (had && had != want)
|
|
176
|
+
clear(rootOf(y.doc, name, had));
|
|
177
|
+
if (want == 'v') {
|
|
178
|
+
if (!same(cur, n))
|
|
179
|
+
y.set(k, n);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (had != want) {
|
|
183
|
+
y.set(k, { $y: want });
|
|
184
|
+
p = undefined;
|
|
185
|
+
}
|
|
186
|
+
t = rootOf(y.doc, name, want);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
if (want == 'v') {
|
|
190
|
+
if (!same(cur, n))
|
|
191
|
+
y.set(k, n);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (had != want) {
|
|
195
|
+
t = want == 't' ? new Y__namespace.Text() : want == 'l' ? new Y__namespace.Array() : new Y__namespace.Map();
|
|
196
|
+
y.set(k, t);
|
|
197
|
+
if (want == 'c')
|
|
198
|
+
t.set('$c', 1);
|
|
199
|
+
p = undefined;
|
|
200
|
+
}
|
|
201
|
+
else
|
|
202
|
+
t = cur;
|
|
203
|
+
}
|
|
204
|
+
if (want == 't') {
|
|
205
|
+
const s = t.toString();
|
|
206
|
+
spliceInto(s, typeof p == 'string' ? p : s, n, strEq, ({ i, del, ins }) => { if (del)
|
|
207
|
+
t.delete(i, del); if (ins)
|
|
208
|
+
t.insert(i, ins); });
|
|
209
|
+
}
|
|
210
|
+
else if (want == 'c')
|
|
211
|
+
writeColl(t, Array.isArray(p) ? p : [], n, o, itemNode(sh), label);
|
|
212
|
+
else if (want == 'm')
|
|
213
|
+
writeMap(t, p, n, o, name, sh, label);
|
|
214
|
+
else {
|
|
215
|
+
const c = t.toArray();
|
|
216
|
+
spliceInto(c, Array.isArray(p) ? p : c, n, same, ({ i, del, ins }) => { if (del)
|
|
217
|
+
t.delete(i, del); if (ins.length)
|
|
218
|
+
t.insert(i, ins); });
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
/** a collection: removed ids deleted, new ones added, changed ones written field by field, then order */
|
|
222
|
+
const writeColl = (m, prev, next, o, sh, label) => {
|
|
223
|
+
const item = (x, p) => {
|
|
224
|
+
if (p === x)
|
|
225
|
+
return;
|
|
226
|
+
const id = String(x.id), it = m.get(id);
|
|
227
|
+
if (it instanceof Y__namespace.Map) {
|
|
228
|
+
if (p !== x)
|
|
229
|
+
writeMap(it, p ?? readItem(it), x, o, undefined, sh, label + '[]');
|
|
230
|
+
}
|
|
231
|
+
// new here. An id this side knew that is gone from the document was deleted remotely while it
|
|
232
|
+
// was edited here: the delete wins (no resurrection)
|
|
233
|
+
else if (!p) {
|
|
234
|
+
const im = new Y__namespace.Map();
|
|
235
|
+
m.set(id, im);
|
|
236
|
+
writeMap(im, {}, x, o, undefined, sh, label + '[]');
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
// the common case (an edit, a toggle): the same ids in the same order. Unchanged items (the same
|
|
240
|
+
// object) are skipped by index, no maps are built and the order is left alone
|
|
241
|
+
if (prev.length == next.length && prev.every((x, i) => x === next[i] || x.id === next[i].id)) {
|
|
242
|
+
for (let i = 0; i < next.length; i++)
|
|
243
|
+
if (prev[i] !== next[i])
|
|
244
|
+
item(next[i], prev[i]);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// items added (an add, a paste) and nothing else moved: only the new items get keys, between
|
|
248
|
+
// their neighbours' (the keys order() would give them)
|
|
249
|
+
if (next.length > prev.length) {
|
|
250
|
+
const added = [];
|
|
251
|
+
let j = 0;
|
|
252
|
+
for (let i = 0; i < next.length; i++) {
|
|
253
|
+
if (j < prev.length && next[i].id === prev[j].id)
|
|
254
|
+
item(next[i], prev[j++]);
|
|
255
|
+
else
|
|
256
|
+
added.push(i);
|
|
257
|
+
}
|
|
258
|
+
if (j == prev.length) {
|
|
259
|
+
for (const i of added)
|
|
260
|
+
item(next[i], undefined);
|
|
261
|
+
if (insertKeys(m, next, added))
|
|
262
|
+
return;
|
|
263
|
+
return order(m, next);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const was = new Map(prev.map((x) => [String(x.id), x])), now = new Set(next.map((x) => String(x.id)));
|
|
267
|
+
for (const id of was.keys())
|
|
268
|
+
if (!now.has(id) && m.has(id))
|
|
269
|
+
m.delete(id);
|
|
270
|
+
for (const x of next)
|
|
271
|
+
item(x, was.get(String(x.id)));
|
|
272
|
+
// the ids or their order changed here (a local edit never reorders, even when the document holds
|
|
273
|
+
// a remote move this side hasn't seen); removals alone leave the others' order as it was
|
|
274
|
+
let j = 0;
|
|
275
|
+
for (const x of prev)
|
|
276
|
+
if (j < next.length && x.id === next[j].id)
|
|
277
|
+
j++;
|
|
278
|
+
if (j < next.length)
|
|
279
|
+
order(m, next);
|
|
280
|
+
};
|
|
281
|
+
// keys for the items at `added` (indexes in next) between their neighbours' keys; false when a
|
|
282
|
+
// neighbour's key is missing or out of order here (a remote move): order() then sorts it out
|
|
283
|
+
const insertKeys = (m, next, added) => {
|
|
284
|
+
const keyAt = (i) => m.get(String(next[i].id))?.get('$o');
|
|
285
|
+
for (let a = 0; a < added.length; a++) {
|
|
286
|
+
const i = added[a], it = m.get(String(next[i].id));
|
|
287
|
+
if (!(it instanceof Y__namespace.Map))
|
|
288
|
+
continue;
|
|
289
|
+
let lo = null, hi = null;
|
|
290
|
+
for (let k = i - 1; k >= 0 && lo == null; k--) {
|
|
291
|
+
const v = keyAt(k);
|
|
292
|
+
if (v !== undefined) {
|
|
293
|
+
if (!valid(v))
|
|
294
|
+
return false;
|
|
295
|
+
lo = v;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// the next item that isn't new (new ones after this one get keys after it)
|
|
299
|
+
for (let k = i + 1, b = a + 1; k < next.length; k++) {
|
|
300
|
+
if (b < added.length && added[b] == k) {
|
|
301
|
+
b++;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const v = keyAt(k);
|
|
305
|
+
if (v === undefined)
|
|
306
|
+
continue;
|
|
307
|
+
if (!valid(v))
|
|
308
|
+
return false;
|
|
309
|
+
hi = v;
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
if (lo != null && hi != null && lo >= hi)
|
|
313
|
+
return false;
|
|
314
|
+
const cur = it.get('$o');
|
|
315
|
+
if (!(valid(cur) && (lo == null || cur > lo) && (hi == null || cur < hi)))
|
|
316
|
+
it.set('$o', fractionalIndexing.generateKeyBetween(lo, hi));
|
|
317
|
+
}
|
|
318
|
+
return true;
|
|
319
|
+
};
|
|
320
|
+
/**
|
|
321
|
+
* The order keys: the longest run of items whose current keys already increase in `next`'s order
|
|
322
|
+
* stays; every other item gets a key between its neighbours (fractional-indexing; deterministic, so
|
|
323
|
+
* two clients seeding the same ids write the same keys). Idempotent: an order the document already
|
|
324
|
+
* has writes nothing; one move writes one key.
|
|
325
|
+
*/
|
|
326
|
+
const order = (m, next) => {
|
|
327
|
+
const items = next.map((x) => m.get(String(x.id))).filter((it) => it instanceof Y__namespace.Map);
|
|
328
|
+
const keys = items.map((it) => { const k = it.get('$o'); return valid(k) ? k : undefined; });
|
|
329
|
+
const keep = lis(keys);
|
|
330
|
+
let last = null;
|
|
331
|
+
items.forEach((it, i) => {
|
|
332
|
+
if (keep.has(i)) {
|
|
333
|
+
last = keys[i];
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
let hi = null;
|
|
337
|
+
for (let j = i + 1; j < items.length; j++)
|
|
338
|
+
if (keep.has(j) && (last == null || keys[j] > last)) {
|
|
339
|
+
hi = keys[j];
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
last = fractionalIndexing.generateKeyBetween(last, hi);
|
|
343
|
+
it.set('$o', last);
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
/** indexes of a longest strictly increasing subsequence (undefined keys never kept) */
|
|
347
|
+
const lis = (keys) => {
|
|
348
|
+
const tails = [], prev = [];
|
|
349
|
+
keys.forEach((k, i) => {
|
|
350
|
+
if (k === undefined)
|
|
351
|
+
return;
|
|
352
|
+
let lo = 0, hi = tails.length;
|
|
353
|
+
while (lo < hi) {
|
|
354
|
+
const mid = (lo + hi) >> 1;
|
|
355
|
+
keys[tails[mid]] < k ? lo = mid + 1 : hi = mid;
|
|
356
|
+
}
|
|
357
|
+
prev[i] = lo ? tails[lo - 1] : -1;
|
|
358
|
+
tails[lo] = i;
|
|
359
|
+
});
|
|
360
|
+
const out = new Set();
|
|
361
|
+
for (let i = tails.length ? tails[tails.length - 1] : -1; i >= 0; i = prev[i])
|
|
362
|
+
out.add(i);
|
|
363
|
+
return out;
|
|
364
|
+
};
|
|
365
|
+
const dirtyHere = (y, path, d) => {
|
|
366
|
+
if (d.types.has(y))
|
|
367
|
+
return true;
|
|
368
|
+
if (path != null)
|
|
369
|
+
for (const n of d.roots)
|
|
370
|
+
if (n.startsWith(path + '.'))
|
|
371
|
+
return true;
|
|
372
|
+
return false;
|
|
373
|
+
};
|
|
374
|
+
/**
|
|
375
|
+
* A slice's JSON: read(doc.getMap(key), key). With `d` and `prev` (the previous read), only what
|
|
376
|
+
* the transaction changed is read again; everything else is prev's object.
|
|
377
|
+
*/
|
|
378
|
+
const read = (y, path, prev, d) => {
|
|
379
|
+
if (!(y instanceof Y__namespace.AbstractType))
|
|
380
|
+
return y;
|
|
381
|
+
if (d && prev !== undefined && !dirtyHere(y, path, d))
|
|
382
|
+
return prev;
|
|
383
|
+
if (y instanceof Y__namespace.Text)
|
|
384
|
+
return y.toString();
|
|
385
|
+
if (y instanceof Y__namespace.Array)
|
|
386
|
+
return reconcile(prev, y.toArray().map((v) => read(v)));
|
|
387
|
+
if (y instanceof Y__namespace.Map) {
|
|
388
|
+
if (isCollMap(y))
|
|
389
|
+
return readColl(y, d && Array.isArray(prev) ? prev : undefined, d);
|
|
390
|
+
const out = {}, p = d && isPlain(prev) ? prev : undefined;
|
|
391
|
+
y.forEach((v, k) => {
|
|
392
|
+
const m = path != null && marker(v);
|
|
393
|
+
out[k] = !m ? read(v, undefined, p?.[k], d)
|
|
394
|
+
: m == 'c' ? readColl(rootOf(y.doc, path + '.' + k, m), d && Array.isArray(p?.[k]) ? p[k] : undefined, d)
|
|
395
|
+
: read(rootOf(y.doc, path + '.' + k, m), path + '.' + k, p?.[k], d);
|
|
396
|
+
});
|
|
397
|
+
// the fields were read against p's: p itself when every one is p's
|
|
398
|
+
if (!p)
|
|
399
|
+
return out;
|
|
400
|
+
let n = 0;
|
|
401
|
+
for (const k in out)
|
|
402
|
+
if (out[k] !== p[k] || !(k in p))
|
|
403
|
+
return out;
|
|
404
|
+
else
|
|
405
|
+
n++;
|
|
406
|
+
return n == Object.keys(p).length ? p : out;
|
|
407
|
+
}
|
|
408
|
+
return y;
|
|
409
|
+
};
|
|
410
|
+
const byKey = (a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0;
|
|
411
|
+
const readColl = (y, prev, d) => {
|
|
412
|
+
if (!d || !prev) {
|
|
413
|
+
const items = [];
|
|
414
|
+
y.forEach((v, k) => { if (v instanceof Y__namespace.Map)
|
|
415
|
+
items.push([v.get('$o') ?? '', k, v]); });
|
|
416
|
+
return items.sort(byKey).map(([, , v]) => readItem(v));
|
|
417
|
+
}
|
|
418
|
+
if (!d.types.has(y))
|
|
419
|
+
return prev;
|
|
420
|
+
// ids to place again (added, removed, replaced, or a new order key) and items edited in place
|
|
421
|
+
const place = new Set(), edited = new Set();
|
|
422
|
+
for (const k of d.changed.get(y) || [])
|
|
423
|
+
if (k != null)
|
|
424
|
+
place.add(k);
|
|
425
|
+
for (const t of d.types)
|
|
426
|
+
if (t._item?.parent === y && t._item.parentSub != null) {
|
|
427
|
+
const id = t._item.parentSub;
|
|
428
|
+
if (d.changed.get(t)?.has('$o'))
|
|
429
|
+
place.add(id);
|
|
430
|
+
else
|
|
431
|
+
edited.add(id);
|
|
432
|
+
}
|
|
433
|
+
// prev is sorted by ($o, id): the items that stay keep their order; the placed ones go in by binary search
|
|
434
|
+
const was = new Map(), out = [];
|
|
435
|
+
for (const x of prev) {
|
|
436
|
+
const id = String(x.id);
|
|
437
|
+
if (place.has(id))
|
|
438
|
+
was.set(id, x);
|
|
439
|
+
else
|
|
440
|
+
out.push(edited.has(id) ? readItem(y.get(id), x, d) : x);
|
|
441
|
+
}
|
|
442
|
+
if (place.size) {
|
|
443
|
+
const key = (x) => [y.get(String(x.id))?.get('$o') ?? '', String(x.id)];
|
|
444
|
+
const ins = [];
|
|
445
|
+
for (const id of place) {
|
|
446
|
+
const v = y.get(id);
|
|
447
|
+
if (v instanceof Y__namespace.Map)
|
|
448
|
+
ins.push([v.get('$o') ?? '', id, v]);
|
|
449
|
+
}
|
|
450
|
+
for (const e of ins.sort(byKey)) {
|
|
451
|
+
let lo = 0, hi = out.length;
|
|
452
|
+
while (lo < hi) {
|
|
453
|
+
const mid = (lo + hi) >> 1;
|
|
454
|
+
byKey(key(out[mid]), e) < 0 ? lo = mid + 1 : hi = mid;
|
|
455
|
+
}
|
|
456
|
+
out.splice(lo, 0, readItem(e[2], was.get(e[1]), d));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return out.length == prev.length && out.every((x, i) => x === prev[i]) ? prev : out;
|
|
460
|
+
};
|
|
461
|
+
const readItem = (it, prev, d) => {
|
|
462
|
+
const x = {};
|
|
463
|
+
it.forEach((v, k) => { if (k != '$o')
|
|
464
|
+
x[k] = read(v, undefined, prev?.[k], d); });
|
|
465
|
+
return prev ? reconcile(prev, x) : x;
|
|
466
|
+
};
|
|
467
|
+
/** next, reusing prev's objects wherever they are deep-equal (Collection items keep their identity) */
|
|
468
|
+
const reconcile = (prev, next) => {
|
|
469
|
+
if (prev === next)
|
|
470
|
+
return prev;
|
|
471
|
+
if (Array.isArray(next)) {
|
|
472
|
+
if (!Array.isArray(prev))
|
|
473
|
+
return next;
|
|
474
|
+
let byId;
|
|
475
|
+
let changed = prev.length != next.length;
|
|
476
|
+
const out = next.map((x, i) => {
|
|
477
|
+
// index first; by id only when the items moved
|
|
478
|
+
let p = prev[i];
|
|
479
|
+
if (x === p)
|
|
480
|
+
return p;
|
|
481
|
+
if (hasId(x) && !(hasId(p) && p.id === x.id))
|
|
482
|
+
p = (byId || (byId = new Map(prev.filter(hasId).map((y) => [y.id, y])))).get(x.id);
|
|
483
|
+
const r = reconcile(p, x);
|
|
484
|
+
if (r !== prev[i])
|
|
485
|
+
changed = true;
|
|
486
|
+
return r;
|
|
487
|
+
});
|
|
488
|
+
return changed ? out : prev;
|
|
489
|
+
}
|
|
490
|
+
if (isPlain(next)) {
|
|
491
|
+
if (!isPlain(prev))
|
|
492
|
+
return next;
|
|
493
|
+
let changed = Object.keys(prev).length != Object.keys(next).length;
|
|
494
|
+
const out = {};
|
|
495
|
+
for (const k in next) {
|
|
496
|
+
out[k] = reconcile(prev[k], next[k]);
|
|
497
|
+
if (out[k] !== prev[k] || !(k in prev))
|
|
498
|
+
changed = true;
|
|
499
|
+
}
|
|
500
|
+
return changed ? out : prev;
|
|
501
|
+
}
|
|
502
|
+
return Object.is(prev, next) ? prev : next;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
/*
|
|
506
|
+
* 'sygnal/yjs' (research: dev-plans/research/crdt-state): makeYjsDriver, the driver that owns a Y.Doc
|
|
507
|
+
* for `shared()` slices.
|
|
508
|
+
*
|
|
509
|
+
* const drivers = { SHARED: makeYjsDriver({ doc, provider, text: ['text'] }) }
|
|
510
|
+
*
|
|
511
|
+
* Sink commands (data, from model entries):
|
|
512
|
+
* { key, value, shape?, seed? } state[key]'s new value (shared()'s `key.LOCAL`, coalesced per flush)
|
|
513
|
+
* { undo: key } / { redo: key } this client's own changes to state[key] (a Y.UndoManager per slice)
|
|
514
|
+
* { key, value, step } (sygnal/canvas history()) a new `step` closes this client's undo
|
|
515
|
+
* step after the write; while steps come, edits merge by step, not by time
|
|
516
|
+
* { presence: { ... } } this client's awareness state (merged)
|
|
517
|
+
* { connect: boolean } provider.connect() / provider.disconnect() (go offline and back)
|
|
518
|
+
* Sources: remote(key) ({ value, base }), presence() (the other clients), history(key)
|
|
519
|
+
* ({ canUndo, canRedo }), doc.
|
|
520
|
+
*
|
|
521
|
+
* - Local: the new value is diffed against the last one the driver saw and written in one
|
|
522
|
+
* transaction (mapping.ts writeMap, a 3-way idempotent write).
|
|
523
|
+
* - Remote: every transaction from elsewhere (a peer, a sync, an undo) updates the driver's mirror of
|
|
524
|
+
* the slice incrementally (only the types it changed are read again) and emits { value, base };
|
|
525
|
+
* shared()'s REMOTE reducer applies it only while state[key] is still `base`.
|
|
526
|
+
* - Seeding (Q3): `seed: 'whenSynced'` (default) waits for the provider's first sync, then seeds an
|
|
527
|
+
* empty document from the app's value, or else takes the document's content (writing any edits
|
|
528
|
+
* the app made before the sync onto it). `seed: 'ifEmpty'` seeds at once (a local-only document).
|
|
529
|
+
* Without `provider` / `synced` the document counts as synced.
|
|
530
|
+
*/
|
|
531
|
+
const report = (code, message, fix, data) => {
|
|
532
|
+
try {
|
|
533
|
+
globalThis.__SYGNAL_DIAGNOSTICS__?.report(code, { severity: 'warn', message, fix, data });
|
|
534
|
+
}
|
|
535
|
+
catch (e) {
|
|
536
|
+
queueMicrotask(() => { throw e; });
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
// the promise of a provider's first sync; null: synced now
|
|
540
|
+
const syncedOf = (o) => {
|
|
541
|
+
if (o.synced)
|
|
542
|
+
return o.synced;
|
|
543
|
+
const p = o.provider;
|
|
544
|
+
if (!p || p.synced === true || p.isSynced === true)
|
|
545
|
+
return null;
|
|
546
|
+
if (p.whenSynced?.then)
|
|
547
|
+
return p.whenSynced;
|
|
548
|
+
if (typeof p.on != 'function')
|
|
549
|
+
return null;
|
|
550
|
+
return new Promise((r) => {
|
|
551
|
+
// y-websocket: 'sync' (true); y-webrtc: 'synced' ({ synced }); Hocuspocus: 'synced' ({ state })
|
|
552
|
+
const f = (x) => {
|
|
553
|
+
if (x === false || x?.synced === false || x?.state === false)
|
|
554
|
+
return;
|
|
555
|
+
for (const e of ['sync', 'synced'])
|
|
556
|
+
p.off?.(e, f);
|
|
557
|
+
r();
|
|
558
|
+
};
|
|
559
|
+
for (const e of ['sync', 'synced'])
|
|
560
|
+
p.on(e, f);
|
|
561
|
+
});
|
|
562
|
+
};
|
|
563
|
+
const makeYjsDriver = (o) => (sink$) => {
|
|
564
|
+
const { doc } = o, provider = o.provider, awareness = o.awareness ?? provider?.awareness;
|
|
565
|
+
const local = {}, seed = {}, slices = new Map(), warned = new Set();
|
|
566
|
+
const opts = {
|
|
567
|
+
text: o.text,
|
|
568
|
+
ambiguous: (path) => {
|
|
569
|
+
if (warned.has(path))
|
|
570
|
+
return;
|
|
571
|
+
warned.add(path);
|
|
572
|
+
report('SYG261', `shared(): '${path}' is an empty array, so its kind is a guess: an id-keyed collection. A list of plain values would be stored as a collection until it has items`, `Declare it: shared({ shape: { ${path.split('.').pop()}: 'list' } }) for plain values, or collection() for items with an id`, { path });
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
const first = syncedOf(o);
|
|
576
|
+
let synced = !first;
|
|
577
|
+
first?.then(() => {
|
|
578
|
+
synced = true;
|
|
579
|
+
for (const s of slices.values())
|
|
580
|
+
if (!s.started && s.first !== undefined)
|
|
581
|
+
start(s);
|
|
582
|
+
});
|
|
583
|
+
// the root-level types' names (doc.share), by type. Read again on a miss: a type that arrived in
|
|
584
|
+
// an update is a placeholder until its first getMap() / getText(), which puts a new object in
|
|
585
|
+
// doc.share under the same name (the size doesn't change)
|
|
586
|
+
let names = new Map();
|
|
587
|
+
const nameOf = (t) => {
|
|
588
|
+
let n = names.get(t);
|
|
589
|
+
if (n === undefined) {
|
|
590
|
+
names = new Map([...doc.share].map(([k, v]) => [v, k]));
|
|
591
|
+
n = names.get(t);
|
|
592
|
+
}
|
|
593
|
+
return n;
|
|
594
|
+
};
|
|
595
|
+
// what a transaction changed (mapping.ts Dirty)
|
|
596
|
+
const dirtyOf = (tr) => {
|
|
597
|
+
const types = new Set(), roots = new Set();
|
|
598
|
+
for (const t of tr.changed.keys()) {
|
|
599
|
+
let r = t;
|
|
600
|
+
types.add(r);
|
|
601
|
+
while (r._item) {
|
|
602
|
+
r = r._item.parent;
|
|
603
|
+
types.add(r);
|
|
604
|
+
}
|
|
605
|
+
const n = nameOf(r);
|
|
606
|
+
if (n)
|
|
607
|
+
roots.add(n);
|
|
608
|
+
}
|
|
609
|
+
return { types, roots, changed: tr.changed };
|
|
610
|
+
};
|
|
611
|
+
const touches = (d, k) => {
|
|
612
|
+
for (const n of d.roots)
|
|
613
|
+
if (n === k || n.startsWith(k + '.'))
|
|
614
|
+
return true;
|
|
615
|
+
return false;
|
|
616
|
+
};
|
|
617
|
+
const onTransaction = (tr) => {
|
|
618
|
+
if (!slices.size || !tr.changed.size)
|
|
619
|
+
return;
|
|
620
|
+
const d = dirtyOf(tr);
|
|
621
|
+
for (const s of slices.values()) {
|
|
622
|
+
if (!s.started || !touches(d, s.key))
|
|
623
|
+
continue;
|
|
624
|
+
s.mirror = read(s.y, s.key, s.mirror, d);
|
|
625
|
+
if (tr.origin !== local && tr.origin !== seed)
|
|
626
|
+
emit(s, !!s.um && tr.origin === s.um);
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
doc.on('afterTransaction', onTransaction);
|
|
630
|
+
const slice = (k) => {
|
|
631
|
+
let s = slices.get(k);
|
|
632
|
+
if (s)
|
|
633
|
+
return s;
|
|
634
|
+
s = { key: k, y: doc.getMap(k), started: false, first: undefined, last: undefined, mirror: undefined, emitted: new Set(), sent: undefined, sentBase: undefined, seed: 'whenSynced', out: new Set() };
|
|
635
|
+
if (o.undo !== false)
|
|
636
|
+
s.um = new Y__namespace.UndoManager(s.y, { trackedOrigins: new Set([local]), captureTimeout: o.captureTimeout ?? 500 });
|
|
637
|
+
slices.set(k, s);
|
|
638
|
+
return s;
|
|
639
|
+
};
|
|
640
|
+
// the slice's path-named root types join its undo scope inside the transaction that may create them
|
|
641
|
+
const scope = (s) => {
|
|
642
|
+
if (s.um)
|
|
643
|
+
for (const [n, t] of doc.share)
|
|
644
|
+
if (n.startsWith(s.key + '.'))
|
|
645
|
+
s.um.addToScope(t);
|
|
646
|
+
};
|
|
647
|
+
const write = (s, prev, next, origin) => doc.transact(() => { writeMap(s.y, prev, next, opts, s.key, s.shape); scope(s); }, origin);
|
|
648
|
+
// the document's content as a REMOTE action, unless the app already has it (or has it on its way)
|
|
649
|
+
const emit = (s, undo = false) => {
|
|
650
|
+
// the mirror takes the app's objects wherever they are equal (Collection items keep their identity)
|
|
651
|
+
if (s.mirror !== s.sent)
|
|
652
|
+
s.mirror = reconcile(s.last, s.mirror);
|
|
653
|
+
const value = s.mirror;
|
|
654
|
+
if (value === s.last || value === s.sent && s.sentBase === s.last)
|
|
655
|
+
return;
|
|
656
|
+
s.sent = value;
|
|
657
|
+
s.sentBase = s.last;
|
|
658
|
+
s.emitted.add(value);
|
|
659
|
+
for (const l of s.out)
|
|
660
|
+
l.next(undo ? { value, base: s.last, undo } : { value, base: s.last });
|
|
661
|
+
};
|
|
662
|
+
// bind the slice to the document: seed an empty one from the app's value, or else take its
|
|
663
|
+
// content (with the app's edits since its first value written onto it)
|
|
664
|
+
const start = (s) => {
|
|
665
|
+
s.started = true;
|
|
666
|
+
const v = s.last;
|
|
667
|
+
if (s.y.size == 0)
|
|
668
|
+
write(s, {}, v, seed);
|
|
669
|
+
else if (v !== s.first)
|
|
670
|
+
write(s, s.first, v, local);
|
|
671
|
+
s.mirror = read(s.y, s.key);
|
|
672
|
+
emit(s);
|
|
673
|
+
};
|
|
674
|
+
/*
|
|
675
|
+
* state[key]'s value: from `key.LOCAL` (coalesced per flush) and from every `key.REMOTE` (its
|
|
676
|
+
* driver sink sends the state the REMOTE reducer committed: the REMOTE value when it applied,
|
|
677
|
+
* else the app's own newer value). So the driver always diffs from a value the app really had.
|
|
678
|
+
*/
|
|
679
|
+
const onLocal = (c) => {
|
|
680
|
+
onValue(c);
|
|
681
|
+
// history() (sygnal/canvas): one undo step per commit, however long the gesture took
|
|
682
|
+
if (c.step === undefined)
|
|
683
|
+
return;
|
|
684
|
+
const s = slice(c.key);
|
|
685
|
+
if (s.um)
|
|
686
|
+
s.um.captureTimeout = Infinity;
|
|
687
|
+
if (c.step !== s.step) {
|
|
688
|
+
if (s.step !== undefined)
|
|
689
|
+
s.um?.stopCapturing();
|
|
690
|
+
s.step = c.step;
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
const onValue = (c) => {
|
|
694
|
+
const s = slice(c.key), v = c.value;
|
|
695
|
+
if (v === undefined)
|
|
696
|
+
return;
|
|
697
|
+
if (!s.started) {
|
|
698
|
+
if (s.first === undefined) {
|
|
699
|
+
s.first = v;
|
|
700
|
+
s.shape = normShape(c.shape);
|
|
701
|
+
s.seed = c.seed || 'whenSynced';
|
|
702
|
+
}
|
|
703
|
+
s.last = v;
|
|
704
|
+
if (synced || s.seed == 'ifEmpty')
|
|
705
|
+
start(s);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
// the app took a REMOTE value (or nothing changed): any newer document content follows
|
|
709
|
+
if (v === s.last || s.emitted.has(v)) {
|
|
710
|
+
// REMOTE actions run in order: the ones sent before this one were answered already
|
|
711
|
+
if (v !== s.last)
|
|
712
|
+
for (const e of s.emitted) {
|
|
713
|
+
s.emitted.delete(e);
|
|
714
|
+
if (e === v)
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
s.last = v;
|
|
718
|
+
return emit(s);
|
|
719
|
+
}
|
|
720
|
+
write(s, s.last, v, local);
|
|
721
|
+
s.last = v;
|
|
722
|
+
s.emitted.clear();
|
|
723
|
+
emit(s);
|
|
724
|
+
};
|
|
725
|
+
sink$.addListener({
|
|
726
|
+
next: (c) => {
|
|
727
|
+
if (!c)
|
|
728
|
+
return;
|
|
729
|
+
if ('value' in c && c.key)
|
|
730
|
+
onLocal(c);
|
|
731
|
+
if (c.undo)
|
|
732
|
+
slice(c.undo).um?.undo();
|
|
733
|
+
if (c.redo)
|
|
734
|
+
slice(c.redo).um?.redo();
|
|
735
|
+
if (c.presence && awareness)
|
|
736
|
+
awareness.setLocalState({ ...awareness.getLocalState(), ...c.presence });
|
|
737
|
+
if (typeof c.connect == 'boolean')
|
|
738
|
+
c.connect ? provider?.connect?.() : provider?.disconnect?.();
|
|
739
|
+
},
|
|
740
|
+
error: () => { },
|
|
741
|
+
complete: () => {
|
|
742
|
+
doc.off('afterTransaction', onTransaction);
|
|
743
|
+
for (const s of slices.values())
|
|
744
|
+
s.um?.destroy();
|
|
745
|
+
},
|
|
746
|
+
});
|
|
747
|
+
const others = () => [...awareness.getStates()].filter(([id]) => id !== awareness.clientID).map(([id, st]) => ({ id, ...st }));
|
|
748
|
+
return {
|
|
749
|
+
/** remote content for state[key], as { value, base } */
|
|
750
|
+
remote: (k) => {
|
|
751
|
+
let l;
|
|
752
|
+
return sygnal.xs.create({ start: (x) => { slice(k).out.add(l = x); }, stop: () => { slice(k).out.delete(l); } });
|
|
753
|
+
},
|
|
754
|
+
/** the other clients' presence states, on every awareness change */
|
|
755
|
+
presence: () => {
|
|
756
|
+
if (!awareness)
|
|
757
|
+
return sygnal.xs.empty();
|
|
758
|
+
let f;
|
|
759
|
+
return sygnal.xs.create({
|
|
760
|
+
start: (l) => { f = () => l.next(others()); f(); awareness.on('change', f); },
|
|
761
|
+
stop: () => { awareness.off('change', f); },
|
|
762
|
+
});
|
|
763
|
+
},
|
|
764
|
+
/** { canUndo, canRedo } for state[key]'s per-user history */
|
|
765
|
+
history: (k) => {
|
|
766
|
+
let f;
|
|
767
|
+
const um = () => slice(k).um, ev = ['stack-item-added', 'stack-item-popped', 'stack-cleared'];
|
|
768
|
+
return sygnal.xs.create({
|
|
769
|
+
start: (l) => {
|
|
770
|
+
const u = um();
|
|
771
|
+
f = () => l.next({ canUndo: !!u?.canUndo(), canRedo: !!u?.canRedo() });
|
|
772
|
+
f();
|
|
773
|
+
for (const e of ev)
|
|
774
|
+
u?.on(e, f);
|
|
775
|
+
},
|
|
776
|
+
stop: () => { for (const e of ev)
|
|
777
|
+
um()?.off(e, f); },
|
|
778
|
+
});
|
|
779
|
+
},
|
|
780
|
+
doc,
|
|
781
|
+
};
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
/*
|
|
785
|
+
* 'sygnal/yjs' (research: dev-plans/research/crdt-state): shared(), the behavior that binds one
|
|
786
|
+
* top-level state key to the Yjs driver's document.
|
|
787
|
+
*
|
|
788
|
+
* Board.uses = { board: shared({ presence: 'peers', history: 'history' }) } // state.board is shared
|
|
789
|
+
* Board.model = {
|
|
790
|
+
* ADD: (state, item) => ({ ...state, board: { ...state.board, items: [...state.board.items, item] } }),
|
|
791
|
+
* UNDO: { SHARED: { undo: 'board' } }, // this user's changes only
|
|
792
|
+
* HERE: { SHARED: (_, id) => ({ presence: { editing: id } }) },
|
|
793
|
+
* }
|
|
794
|
+
*
|
|
795
|
+
* The model stays the only writer of state:
|
|
796
|
+
* - `board.LOCAL`: every new value of state.board reaches the driver as data, once per flush (the
|
|
797
|
+
* values of one flush coalesce: a burst of actions is one Yjs transaction).
|
|
798
|
+
* - `board.REMOTE` ({ value, base }): a document change from elsewhere; it applies only while
|
|
799
|
+
* state.board is still `base` (no unsynced local edit is overwritten), and its driver sink tells
|
|
800
|
+
* the driver the state it committed either way.
|
|
801
|
+
* - `board.PRESENCE` / `board.HISTORY` with `presence: 'peers'` / `history: 'history'`: the other
|
|
802
|
+
* clients' awareness states and { canUndo, canRedo } in those state keys.
|
|
803
|
+
* LOCAL and REMOTE are marked as built-in actions (`Symbol.for('sygnal.builtIn')` on their
|
|
804
|
+
* handlers): the action log calls them 'built-in', so DevTools hides them with INITIALIZE (Q5).
|
|
805
|
+
*
|
|
806
|
+
* `ownsState: false`: the host's initialState keeps state[key] (the first value seeds an empty
|
|
807
|
+
* document); the behavior adds no slice of its own, and sygnal-check's SYG127 reads the marker.
|
|
808
|
+
* `undo()` on the same key reverts other users' changes: SYG260 (dev) points to `{ SHARED: { undo } }`.
|
|
809
|
+
*
|
|
810
|
+
* sygnal/canvas's `history(key)` routes here (maintainer decision 7, one API): model entries that
|
|
811
|
+
* are its `undo` / `redo` become `{ [driver]: { undo: key } }` / `{ redo: key }`, its `commit`
|
|
812
|
+
* bumps `state[step]` instead of keeping snapshots, and the LOCAL command carries that step, so the
|
|
813
|
+
* driver closes this user's undo step at each commit (one step per gesture). `after` runs on the
|
|
814
|
+
* state an undo / redo brings back. The marker is `Symbol.for('sygnal.history')`: no import.
|
|
815
|
+
*/
|
|
816
|
+
const BUILT_IN = Symbol.for('sygnal.builtIn');
|
|
817
|
+
const HISTORY = Symbol.for('sygnal.history');
|
|
818
|
+
const builtIn = (f) => (f[BUILT_IN] = true, f);
|
|
819
|
+
// SYG260: an undo() behavior of the host whose `key` is this shared key (reported once per use)
|
|
820
|
+
const reported = new WeakSet();
|
|
821
|
+
const checkUndo = (c, k) => {
|
|
822
|
+
const d = globalThis.__SYGNAL_DIAGNOSTICS__, u = c.view?.uses;
|
|
823
|
+
if (!d || !u)
|
|
824
|
+
return;
|
|
825
|
+
for (const n in u) {
|
|
826
|
+
const b = u[n], key = b?.options?.key;
|
|
827
|
+
if (!b?.model?.UNDO || !b.model.REDO || !(key === k || Array.isArray(key) && key.includes(k)) || reported.has(b))
|
|
828
|
+
continue;
|
|
829
|
+
reported.add(b);
|
|
830
|
+
try {
|
|
831
|
+
d.report('SYG260', {
|
|
832
|
+
severity: 'warn', component: c, data: { key: k, uses: n },
|
|
833
|
+
message: `undo() (uses key '${n}') records '${k}', which shared() syncs: its UNDO restores a snapshot of the whole slice and reverts other users' changes made since`,
|
|
834
|
+
fix: `Remove undo() for '${k}' and use the driver's per-user undo: UNDO: { SHARED: { undo: '${k}' } }, REDO: { SHARED: { redo: '${k}' } }, with shared({ history: 'history' }) for canUndo / canRedo`,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
catch (e) {
|
|
838
|
+
queueMicrotask(() => { throw e; });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
/** `uses = { [key]: shared(options) }`: state[key] lives in the Yjs driver's document */
|
|
843
|
+
const shared = (opts = {}) => ({
|
|
844
|
+
options: opts,
|
|
845
|
+
ownsState: false,
|
|
846
|
+
merge(c, k) {
|
|
847
|
+
const D = opts.driver || 'SHARED', S = c.stateSourceName || 'STATE', model = c.model = { ...c.model };
|
|
848
|
+
const { shape, seed } = opts;
|
|
849
|
+
checkUndo(c, k);
|
|
850
|
+
// sygnal/canvas history(k): its undo / redo entries go to this user's UndoManager
|
|
851
|
+
let hist = null;
|
|
852
|
+
for (const name in model) {
|
|
853
|
+
const e = model[name], f = typeof e == 'function' ? e : e?.[S], h = f?.[HISTORY];
|
|
854
|
+
if (!h || h.field !== k)
|
|
855
|
+
continue;
|
|
856
|
+
hist = h;
|
|
857
|
+
const cmd = f === h.undo ? { undo: k } : { redo: k }, rest = typeof e == 'function' ? {} : { ...e };
|
|
858
|
+
delete rest[S];
|
|
859
|
+
model[name] = { ...rest, [D]: () => cmd };
|
|
860
|
+
}
|
|
861
|
+
if (hist)
|
|
862
|
+
hist.shared = { history: opts.history };
|
|
863
|
+
const step = hist?.step;
|
|
864
|
+
// the committed slice when the action runs (not the action's data: a REMOTE queued before it may have moved it on)
|
|
865
|
+
model[k + '.LOCAL'] = { [D]: builtIn((s) => step ? { key: k, value: s?.[k], shape, seed, step: s?.[step] ?? 0 } : { key: k, value: s?.[k], shape, seed }) };
|
|
866
|
+
model[k + '.REMOTE'] = {
|
|
867
|
+
[S]: builtIn((s, d) => s?.[k] !== d.base ? sygnal.ABORT : d.undo && hist ? hist.after({ ...s, [k]: d.value }) : { ...s, [k]: d.value }),
|
|
868
|
+
// the state the REMOTE committed (its value, or the app's own newer one) is the driver's new base
|
|
869
|
+
[D]: builtIn((s) => ({ key: k, value: s?.[k] })),
|
|
870
|
+
};
|
|
871
|
+
if (opts.presence)
|
|
872
|
+
model[k + '.PRESENCE'] = { [S]: (s, peers) => ({ ...s, [opts.presence]: peers }) };
|
|
873
|
+
if (opts.history)
|
|
874
|
+
model[k + '.HISTORY'] = { [S]: (s, h) => ({ ...s, [opts.history]: h }) };
|
|
875
|
+
const own = c.intent, owned = c._behaviorActions || (c._behaviorActions = {});
|
|
876
|
+
for (const a of ['LOCAL', 'REMOTE', 'PRESENCE', 'HISTORY'])
|
|
877
|
+
owned[k + '.' + a] = k;
|
|
878
|
+
c.intent = (so) => {
|
|
879
|
+
// no Yjs driver (renderComponent without one: a fake source): the reducers still run
|
|
880
|
+
const h = own?.(so) || {}, st = so[S], src = typeof so[D]?.remote == 'function' ? so[D] : null;
|
|
881
|
+
// state[key]'s latest value once per flush (a microtask after the first change of a burst)
|
|
882
|
+
let prev, latest, queued = false, sub, prevStep, latestStep;
|
|
883
|
+
const local$ = sygnal.xs.create({
|
|
884
|
+
start: (l) => {
|
|
885
|
+
sub = st.stream.subscribe({
|
|
886
|
+
next: (s) => {
|
|
887
|
+
latest = s?.[k];
|
|
888
|
+
if (step)
|
|
889
|
+
latestStep = s?.[step];
|
|
890
|
+
if (queued)
|
|
891
|
+
return;
|
|
892
|
+
queued = true;
|
|
893
|
+
queueMicrotask(() => {
|
|
894
|
+
queued = false;
|
|
895
|
+
if (sub && latest !== undefined && (latest !== prev || latestStep !== prevStep))
|
|
896
|
+
prevStep = latestStep, l.next(prev = latest);
|
|
897
|
+
});
|
|
898
|
+
},
|
|
899
|
+
error: () => { }, complete: () => { },
|
|
900
|
+
});
|
|
901
|
+
},
|
|
902
|
+
stop: () => { sub?.unsubscribe(); sub = undefined; },
|
|
903
|
+
});
|
|
904
|
+
const o = {
|
|
905
|
+
[k + '.LOCAL']: local$,
|
|
906
|
+
[k + '.REMOTE']: src ? src.remote(k) : sygnal.xs.never(),
|
|
907
|
+
...(opts.presence && { [k + '.PRESENCE']: src ? src.presence() : sygnal.xs.never() }),
|
|
908
|
+
...(opts.history && { [k + '.HISTORY']: src ? src.history(k) : sygnal.xs.never() }),
|
|
909
|
+
// the host's own actions (and renderComponent's simulateAction streams) win
|
|
910
|
+
...h,
|
|
911
|
+
};
|
|
912
|
+
// renderComponent's marker (non-enumerable) for a behavior merged after this one
|
|
913
|
+
if (h.__sygnalTestActions)
|
|
914
|
+
Object.defineProperty(o, '__sygnalTestActions', { value: h.__sygnalTestActions });
|
|
915
|
+
return o;
|
|
916
|
+
};
|
|
917
|
+
},
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
exports.collection = collection;
|
|
921
|
+
exports.makeYjsDriver = makeYjsDriver;
|
|
922
|
+
exports.shared = shared;
|
|
923
|
+
//# sourceMappingURL=yjs.cjs.js.map
|