@yahoo/uds-create-config 2.45.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -0
- package/dist/ai-lanes.d.ts +23 -0
- package/dist/ai-lanes.js +34 -0
- package/dist/authoring/define-component.d.ts +85 -0
- package/dist/configs/CanvasConfig.d.ts +3237 -0
- package/dist/configs/CanvasConfig.js +332 -0
- package/dist/configs/build-options.d.ts +10 -0
- package/dist/configs/build-options.js +68 -0
- package/dist/configs/platform.d.ts +15 -0
- package/dist/configs/platform.js +17 -0
- package/dist/configs/react-native-system.d.ts +3026 -0
- package/dist/configs/react-native-system.js +64 -0
- package/dist/configs/system.d.ts +2771 -0
- package/dist/configs/system.js +74 -0
- package/dist/css/properties.generated.d.ts +14 -0
- package/dist/css/properties.generated.js +6260 -0
- package/dist/css/value-type.d.ts +48 -0
- package/dist/css/value-type.js +131 -0
- package/dist/css/values.d.ts +33 -0
- package/dist/css/values.js +116 -0
- package/dist/entities/native/NativeModifier.d.ts +25 -0
- package/dist/entities/native/NativeModifier.js +41 -0
- package/dist/entities/native/NativeSettings.d.ts +44 -0
- package/dist/entities/native/NativeSettings.js +39 -0
- package/dist/entities/native/NativeStyleProperty.d.ts +355 -0
- package/dist/entities/native/NativeStyleProperty.js +28 -0
- package/dist/entities/native/NativeToken.d.ts +31 -0
- package/dist/entities/native/NativeToken.js +49 -0
- package/dist/entities/system/CanvasRole.d.ts +34 -0
- package/dist/entities/system/CanvasRole.js +45 -0
- package/dist/entities/system/Component.d.ts +1287 -0
- package/dist/entities/system/Component.js +1840 -0
- package/dist/entities/system/Composite.d.ts +12 -0
- package/dist/entities/system/Composite.js +26 -0
- package/dist/entities/system/Device.d.ts +11 -0
- package/dist/entities/system/Device.js +18 -0
- package/dist/entities/system/File.d.ts +72 -0
- package/dist/entities/system/File.js +136 -0
- package/dist/entities/system/Font.d.ts +48 -0
- package/dist/entities/system/Font.js +56 -0
- package/dist/entities/system/GlobalStyle.d.ts +26 -0
- package/dist/entities/system/GlobalStyle.js +54 -0
- package/dist/entities/system/Guidance.d.ts +85 -0
- package/dist/entities/system/Guidance.js +59 -0
- package/dist/entities/system/GuidanceStyle.d.ts +14 -0
- package/dist/entities/system/GuidanceStyle.js +8 -0
- package/dist/entities/system/Icon.d.ts +46 -0
- package/dist/entities/system/Icon.js +46 -0
- package/dist/entities/system/LinkedSystem.d.ts +504 -0
- package/dist/entities/system/LinkedSystem.js +814 -0
- package/dist/entities/system/Modifier.d.ts +92 -0
- package/dist/entities/system/Modifier.js +120 -0
- package/dist/entities/system/Motion.d.ts +28 -0
- package/dist/entities/system/Motion.js +48 -0
- package/dist/entities/system/Operation.d.ts +100 -0
- package/dist/entities/system/Operation.js +236 -0
- package/dist/entities/system/Package.d.ts +42 -0
- package/dist/entities/system/Package.js +75 -0
- package/dist/entities/system/Settings.d.ts +94 -0
- package/dist/entities/system/Settings.js +166 -0
- package/dist/entities/system/StyleProperty.d.ts +135 -0
- package/dist/entities/system/StyleProperty.js +438 -0
- package/dist/entities/system/Token.d.ts +67 -0
- package/dist/entities/system/Token.js +129 -0
- package/dist/entities/system/Tool.d.ts +134 -0
- package/dist/entities/system/Tool.js +124 -0
- package/dist/entities/system/color.d.ts +64 -0
- package/dist/entities/system/color.js +123 -0
- package/dist/entities/system/component-module.d.ts +63 -0
- package/dist/entities/system/component-module.js +258 -0
- package/dist/entities/system/copy-plan.d.ts +98 -0
- package/dist/entities/system/copy-plan.js +451 -0
- package/dist/entities/system/defineComponent.d.ts +428 -0
- package/dist/entities/system/defineComponent.js +337 -0
- package/dist/entities/system/element.d.ts +101 -0
- package/dist/entities/system/element.js +171 -0
- package/dist/entities/system/icon-library.d.ts +64 -0
- package/dist/entities/system/icon-library.js +112 -0
- package/dist/entities/system/icon-metadata.d.ts +72 -0
- package/dist/entities/system/icon-metadata.js +111 -0
- package/dist/entities/system/link-declaration.d.ts +27 -0
- package/dist/entities/system/link-declaration.js +64 -0
- package/dist/entities/system/link-plan.d.ts +160 -0
- package/dist/entities/system/link-plan.js +770 -0
- package/dist/entities/system/runtime-modules.d.ts +25 -0
- package/dist/entities/system/runtime-modules.js +38 -0
- package/dist/entities/system/style-bag.d.ts +20 -0
- package/dist/entities/system/style-bag.js +246 -0
- package/dist/entities/system/token-index.d.ts +89 -0
- package/dist/entities/system/token-index.js +112 -0
- package/dist/framework/Config.d.ts +2472 -0
- package/dist/framework/Config.js +5023 -0
- package/dist/framework/Entity.d.ts +127 -0
- package/dist/framework/Entity.js +125 -0
- package/dist/framework/authoring.d.ts +24 -0
- package/dist/framework/authoring.js +7 -0
- package/dist/framework/change-hooks.d.ts +119 -0
- package/dist/framework/change-hooks.js +131 -0
- package/dist/framework/changes.d.ts +88 -0
- package/dist/framework/changes.js +246 -0
- package/dist/framework/class-names.d.ts +260 -0
- package/dist/framework/class-names.js +630 -0
- package/dist/framework/coalesce.d.ts +25 -0
- package/dist/framework/coalesce.js +60 -0
- package/dist/framework/config-op.d.ts +76 -0
- package/dist/framework/config-op.js +43 -0
- package/dist/framework/config-operation-kind.d.ts +14 -0
- package/dist/framework/config-operation-kind.js +14 -0
- package/dist/framework/config-ops.d.ts +34 -0
- package/dist/framework/config-ops.js +187 -0
- package/dist/framework/defineConfig.d.ts +196 -0
- package/dist/framework/defineConfig.js +100 -0
- package/dist/framework/defineDerivedEntity.d.ts +74 -0
- package/dist/framework/defineDerivedEntity.js +83 -0
- package/dist/framework/defineEntity.d.ts +318 -0
- package/dist/framework/defineEntity.js +183 -0
- package/dist/framework/defineSubEntity.d.ts +148 -0
- package/dist/framework/defineSubEntity.js +121 -0
- package/dist/framework/derived-mutations.d.ts +20 -0
- package/dist/framework/derived-mutations.js +38 -0
- package/dist/framework/entity-search.d.ts +29 -0
- package/dist/framework/entity-search.js +117 -0
- package/dist/framework/inline-sources.d.ts +49 -0
- package/dist/framework/inline-sources.js +388 -0
- package/dist/framework/layer-styles.d.ts +25 -0
- package/dist/framework/layer-styles.js +49 -0
- package/dist/framework/memo.d.ts +48 -0
- package/dist/framework/memo.js +84 -0
- package/dist/framework/overlay.d.ts +49 -0
- package/dist/framework/overlay.js +17 -0
- package/dist/framework/plan.d.ts +49 -0
- package/dist/framework/plan.js +73 -0
- package/dist/framework/projections.d.ts +480 -0
- package/dist/framework/projections.js +974 -0
- package/dist/framework/prop-surface.d.ts +132 -0
- package/dist/framework/prop-surface.js +278 -0
- package/dist/framework/ref-graph.d.ts +71 -0
- package/dist/framework/ref-graph.js +158 -0
- package/dist/framework/ref-integrity.d.ts +55 -0
- package/dist/framework/ref-integrity.js +171 -0
- package/dist/framework/registered.d.ts +95 -0
- package/dist/framework/registry-dir.d.ts +42 -0
- package/dist/framework/registry-dir.js +48 -0
- package/dist/framework/registry.d.ts +300 -0
- package/dist/framework/registry.js +104 -0
- package/dist/framework/rejection.d.ts +58 -0
- package/dist/framework/rejection.js +111 -0
- package/dist/framework/rename-inference.d.ts +26 -0
- package/dist/framework/rename-inference.js +50 -0
- package/dist/framework/render-spec.d.ts +296 -0
- package/dist/framework/render-spec.js +809 -0
- package/dist/framework/schema-version.d.ts +140 -0
- package/dist/framework/schema-version.js +214 -0
- package/dist/framework/schemas.d.ts +109 -0
- package/dist/framework/schemas.js +615 -0
- package/dist/framework/session.d.ts +196 -0
- package/dist/framework/session.js +367 -0
- package/dist/framework/signature.d.ts +15 -0
- package/dist/framework/signature.js +83 -0
- package/dist/framework/snapshot.d.ts +115 -0
- package/dist/framework/snapshot.js +277 -0
- package/dist/framework/source-integrity.d.ts +226 -0
- package/dist/framework/source-integrity.js +478 -0
- package/dist/framework/sources.d.ts +211 -0
- package/dist/framework/sources.js +89 -0
- package/dist/framework/utils/compiled-schema.d.ts +23 -0
- package/dist/framework/utils/compiled-schema.js +46 -0
- package/dist/framework/utils/content-key.d.ts +15 -0
- package/dist/framework/utils/content-key.js +33 -0
- package/dist/framework/utils/enumerated.d.ts +13 -0
- package/dist/framework/utils/enumerated.js +19 -0
- package/dist/framework/utils/field-path.d.ts +95 -0
- package/dist/framework/utils/field-path.js +136 -0
- package/dist/framework/utils/field-schema.d.ts +55 -0
- package/dist/framework/utils/field-schema.js +183 -0
- package/dist/framework/utils/group.d.ts +37 -0
- package/dist/framework/utils/group.js +13 -0
- package/dist/framework/utils/package-path.d.ts +37 -0
- package/dist/framework/utils/package-path.js +61 -0
- package/dist/framework/utils/refs.d.ts +194 -0
- package/dist/framework/utils/refs.js +403 -0
- package/dist/framework/validate-spec.d.ts +27 -0
- package/dist/framework/validate-spec.js +45 -0
- package/dist/framework/value-domain.d.ts +26 -0
- package/dist/framework/value-domain.js +202 -0
- package/dist/framework/views-facade.d.ts +15 -0
- package/dist/framework/views-facade.js +19 -0
- package/dist/index.d.ts +90 -42
- package/dist/index.js +86 -37
- package/dist/migrations/2.0.0/raw-css.d.ts +11 -0
- package/dist/migrations/2.0.0/raw-css.js +72 -0
- package/dist/migrations/2.0.0/v1-artifact.d.ts +1559 -0
- package/dist/migrations/2.0.0/v1-artifact.js +1018 -0
- package/dist/migrations/20260908171448_settings_as_kind.d.ts +22 -0
- package/dist/migrations/20260908171448_settings_as_kind.js +89 -0
- package/dist/migrations/20260908194245_visible_state_pointer.d.ts +19 -0
- package/dist/migrations/20260908194245_visible_state_pointer.js +115 -0
- package/dist/migrations/20260909183001_icon_knockout_variants.d.ts +18 -0
- package/dist/migrations/20260909183001_icon_knockout_variants.js +8 -0
- package/dist/migrations/20260910212453_linked_system_link_ops.d.ts +13 -0
- package/dist/migrations/20260910212453_linked_system_link_ops.js +8 -0
- package/dist/migrations/index.d.ts +2 -0
- package/dist/migrations/index.js +2 -0
- package/dist/react-native/style-keys.generated.d.ts +10 -0
- package/dist/react-native/style-keys.generated.js +167 -0
- package/dist/renderer/RendererErrorBoundary.d.ts +32 -5
- package/dist/renderer/RendererErrorBoundary.js +59 -10
- package/dist/renderer/UdsRenderer.d.ts +11 -10
- package/dist/renderer/UdsRenderer.js +1 -1
- package/dist/renderer/assetEntries.d.ts +37 -0
- package/dist/renderer/assetEntries.js +39 -0
- package/dist/renderer/assetRenderable.d.ts +14 -6
- package/dist/renderer/assetRenderable.js +8 -2
- package/dist/renderer/componentEntry.d.ts +58 -0
- package/dist/renderer/componentEntry.js +71 -0
- package/dist/renderer/icon-library.d.ts +20 -0
- package/dist/renderer/index.d.ts +7 -7
- package/dist/renderer/index.js +5 -5
- package/dist/renderer/primitives/SlotRenderer.d.ts +1 -1
- package/dist/renderer/primitives/SlotRenderer.js +1 -1
- package/dist/renderer/runtimeComponent.d.ts +33 -0
- package/dist/renderer/runtimeComponent.js +113 -0
- package/dist/renderer/spec-content.d.ts +43 -0
- package/dist/renderer/spec-content.js +95 -0
- package/dist/renderer/wrapRegistry.d.ts +22 -37
- package/dist/renderer/wrapRegistry.js +10 -20
- package/dist/renderer/wrappers/event-bridge.js +6 -5
- package/dist/renderer/wrappers/hex-normalize.js +6 -5
- package/dist/renderer/wrappers/html-aliases.d.ts +7 -2
- package/dist/renderer/wrappers/html-aliases.js +36 -19
- package/dist/renderer/wrappers/inline-styles.js +7 -6
- package/dist/spec/asset-jsx.d.ts +18 -5
- package/dist/spec/asset-jsx.js +27 -16
- package/dist/spec/collapse-text-labels.d.ts +5 -15
- package/dist/spec/collapse-text-labels.js +26 -19
- package/dist/spec/empty-node-slots.d.ts +56 -34
- package/dist/spec/empty-node-slots.js +87 -47
- package/dist/spec/index.d.ts +21 -10
- package/dist/spec/index.js +21 -10
- package/dist/spec/jsxToSpec.d.ts +14 -9
- package/dist/spec/jsxToSpec.js +20 -3
- package/dist/spec/preview-elements.d.ts +63 -0
- package/dist/spec/preview-elements.js +89 -0
- package/dist/spec/rewrite-refs.d.ts +58 -0
- package/dist/spec/rewrite-refs.js +250 -0
- package/dist/spec/specToJsx.js +18 -29
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +39 -52
- package/dist/AssetGroup.d.ts +0 -77
- package/dist/AssetGroup.js +0 -125
- package/dist/Component.d.ts +0 -333
- package/dist/Component.js +0 -1061
- package/dist/ComponentGroup.d.ts +0 -22
- package/dist/ComponentGroup.js +0 -51
- package/dist/CompositeStyle.d.ts +0 -30
- package/dist/CompositeStyle.js +0 -59
- package/dist/Config.d.ts +0 -528
- package/dist/Config.js +0 -1712
- package/dist/Mode.d.ts +0 -41
- package/dist/Mode.js +0 -81
- package/dist/Modifier.d.ts +0 -51
- package/dist/Modifier.js +0 -97
- package/dist/MotionDef.d.ts +0 -81
- package/dist/MotionDef.js +0 -212
- package/dist/Props.d.ts +0 -332
- package/dist/Props.js +0 -35
- package/dist/Provider.d.ts +0 -20
- package/dist/Provider.js +0 -14
- package/dist/StyleProp.d.ts +0 -145
- package/dist/StyleProp.js +0 -239
- package/dist/Token.d.ts +0 -76
- package/dist/Token.js +0 -133
- package/dist/TokenGroup.d.ts +0 -33
- package/dist/TokenGroup.js +0 -69
- package/dist/asset-kind.d.ts +0 -55
- package/dist/asset-kind.js +0 -29
- package/dist/asset-value.d.ts +0 -23
- package/dist/asset-value.js +0 -40
- package/dist/brands.d.ts +0 -30
- package/dist/brands.js +0 -20
- package/dist/captureCallerPath.d.ts +0 -48
- package/dist/captureCallerPath.js +0 -95
- package/dist/colorExpressions.d.ts +0 -131
- package/dist/colorExpressions.js +0 -148
- package/dist/component-referrers.d.ts +0 -30
- package/dist/component-referrers.js +0 -40
- package/dist/config-diagnostics.d.ts +0 -21
- package/dist/config-diagnostics.js +0 -12
- package/dist/defineAssetGroup.d.ts +0 -200
- package/dist/defineAssetGroup.js +0 -323
- package/dist/defineProvider.d.ts +0 -29
- package/dist/defineProvider.js +0 -60
- package/dist/element-marker.d.ts +0 -63
- package/dist/element-marker.js +0 -113
- package/dist/entity-utils.d.ts +0 -56
- package/dist/entity-utils.js +0 -105
- package/dist/extension.d.ts +0 -55
- package/dist/extension.js +0 -214
- package/dist/factories.d.ts +0 -863
- package/dist/factories.js +0 -475
- package/dist/font-face.d.ts +0 -25
- package/dist/font-face.js +0 -72
- package/dist/foreign-component-name.d.ts +0 -21
- package/dist/foreign-component-name.js +0 -42
- package/dist/interpolate.d.ts +0 -20
- package/dist/interpolate.js +0 -10
- package/dist/jsx/__fixtures__/cross-component-preview.d.ts +0 -3
- package/dist/jsx/__fixtures__/cross-component-preview.js +0 -15
- package/dist/jsx/jsx-dev-runtime.d.ts +0 -15
- package/dist/jsx/jsx-dev-runtime.js +0 -11
- package/dist/jsx/jsx-runtime.d.ts +0 -48
- package/dist/jsx/jsx-runtime.js +0 -305
- package/dist/linked-system-pins.d.ts +0 -30
- package/dist/linked-system-pins.js +0 -45
- package/dist/linkedNodePrefixes.d.ts +0 -73
- package/dist/linkedNodePrefixes.js +0 -99
- package/dist/markers.d.ts +0 -235
- package/dist/markers.js +0 -70
- package/dist/motion-constants.d.ts +0 -37
- package/dist/motion-constants.js +0 -61
- package/dist/native/NativeConfig.d.ts +0 -56
- package/dist/native/NativeConfig.js +0 -162
- package/dist/native/assets.d.ts +0 -20
- package/dist/native/assets.js +0 -17
- package/dist/native/components.d.ts +0 -139
- package/dist/native/components.js +0 -67
- package/dist/native/index.d.ts +0 -18
- package/dist/native/index.js +0 -17
- package/dist/native/modes.d.ts +0 -10
- package/dist/native/modes.js +0 -11
- package/dist/native/types.d.ts +0 -95
- package/dist/native/types.js +0 -0
- package/dist/native/validation.d.ts +0 -8
- package/dist/native/validation.js +0 -885
- package/dist/native/values.d.ts +0 -7
- package/dist/native/values.js +0 -15
- package/dist/native.d.ts +0 -12
- package/dist/native.js +0 -10
- package/dist/refs.d.ts +0 -204
- package/dist/refs.js +0 -129
- package/dist/registry-aliases.d.ts +0 -33
- package/dist/registry-aliases.js +0 -35
- package/dist/registry-dir.d.ts +0 -22
- package/dist/registry-dir.js +0 -10
- package/dist/renderer/makeRegistry.d.ts +0 -34
- package/dist/renderer/makeRegistry.js +0 -52
- package/dist/renderer/makeUdsRenderer.d.ts +0 -13
- package/dist/renderer/makeUdsRenderer.js +0 -51
- package/dist/renderer/wrappers/component-slots.d.ts +0 -41
- package/dist/renderer/wrappers/component-slots.js +0 -66
- package/dist/renderer/wrappers/slot-resolution.d.ts +0 -25
- package/dist/renderer/wrappers/slot-resolution.js +0 -68
- package/dist/renderer/wrappers/void-elements.d.ts +0 -37
- package/dist/renderer/wrappers/void-elements.js +0 -40
- package/dist/spec/layer-props.d.ts +0 -52
- package/dist/spec/layer-props.js +0 -149
- package/dist/spec/preview-controls.d.ts +0 -44
- package/dist/spec/preview-controls.js +0 -139
- package/dist/spec/slot-refs.d.ts +0 -39
- package/dist/spec/slot-refs.js +0 -56
- package/dist/token-override-rows.d.ts +0 -68
- package/dist/token-override-rows.js +0 -233
- package/dist/tokenValueType.d.ts +0 -34
- package/dist/tokenValueType.js +0 -138
- package/dist/types/css-properties.d.ts +0 -233
- package/dist/types/css-properties.js +0 -14
- package/dist/types/css-property-keywords.d.ts +0 -156
- package/dist/types/css-property-keywords.js +0 -616
- package/dist/types/css-values.d.ts +0 -63
- package/dist/types/css-values.js +0 -16
- package/dist/types.d.ts +0 -1138
- package/dist/types.js +0 -12
- package/dist/units.d.ts +0 -14
- package/dist/units.js +0 -16
- package/dist/utils/index.d.ts +0 -4
- package/dist/utils/index.js +0 -4
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { RebaseConflict, StampedPatch } from "./overlay.js";
|
|
2
|
+
import { Snapshot } from "./snapshot.js";
|
|
3
|
+
import { Config, LiveConfig, Patch } from "./Config.js";
|
|
4
|
+
import { ChangeHookMap, ChangeHookRun } from "./change-hooks.js";
|
|
5
|
+
import { WalkOptions } from "./schema-version.js";
|
|
6
|
+
|
|
7
|
+
//#region src/framework/session.d.ts
|
|
8
|
+
/** Confirmed state arriving from storage. */
|
|
9
|
+
interface SourceChange {
|
|
10
|
+
readonly snapshot: Snapshot;
|
|
11
|
+
/** Ids of local patches the new base has absorbed — a merge landing. They are dropped rather than
|
|
12
|
+
* replayed, because the base already contains them. Empty for an ordinary change. */
|
|
13
|
+
readonly merged?: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
/** Where confirmed state comes from — the base half of a branch: the artifact a push built. */
|
|
16
|
+
interface ConfigSource {
|
|
17
|
+
snapshot(): Snapshot;
|
|
18
|
+
subscribe(onChange: (change: SourceChange) => void): () => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Where the shared draft lives — the branch's unmerged patches, as one ordered list.
|
|
22
|
+
*
|
|
23
|
+
* Separate from {@link ConfigSource} because a branch's two sources carry different things: the
|
|
24
|
+
* base is entity state, the draft is patches. Only the draft is collaborative, so only the draft
|
|
25
|
+
* has a live shared home (the `system:{branchId}` Y.Doc); the base is the same artifact for every
|
|
26
|
+
* client and moves only when a push publishes a new one.
|
|
27
|
+
*
|
|
28
|
+
* The unit is the whole list, not a delta. A client adopts it rather than reconciling entry by
|
|
29
|
+
* entry, which is what makes its own echo a no-op — see `Config.withDraft`.
|
|
30
|
+
*/
|
|
31
|
+
interface DraftSource {
|
|
32
|
+
draft(): readonly StampedPatch[];
|
|
33
|
+
subscribe(onChange: (draft: readonly StampedPatch[]) => void): () => void;
|
|
34
|
+
}
|
|
35
|
+
/** Where local patches go. Called once per batch, never once per patch. */
|
|
36
|
+
interface ConfigSink {
|
|
37
|
+
commit(patches: readonly StampedPatch[], next: Config): void | Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Take named entries back out of the shared draft — the other half of the pair, and the reason it
|
|
40
|
+
* has to exist: `drop(ids)` filters a list only this client can see. A patch that had already
|
|
41
|
+
* reached the sink therefore came back on the next change notification, because a client adopts the
|
|
42
|
+
* shared list wholesale (see {@link DraftSource}). Wholesale adoption is what makes an echo a no-op
|
|
43
|
+
* and what resurrects a removal that was never written through.
|
|
44
|
+
*
|
|
45
|
+
* ID-addressed on purpose. It names its victims, so an incomplete local view can't delete anything
|
|
46
|
+
* it didn't mean to. It must never grow into a `replaceAll(desiredList)` convenience: a full set
|
|
47
|
+
* deletes by omission, and omission is indistinguishable from ignorance.
|
|
48
|
+
*
|
|
49
|
+
* Optional, so a sink that only appends — a test double, a local-only session — still satisfies
|
|
50
|
+
* this. `drop` then stays local, exactly as it behaved before this existed.
|
|
51
|
+
*
|
|
52
|
+
* Must be idempotent, like `commit`: retracting an absent id is a no-op, so a retry and two clients
|
|
53
|
+
* retracting the same id both converge.
|
|
54
|
+
*/
|
|
55
|
+
retract?(patchIds: readonly string[]): void | Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/** Which replay dropped an edit — see {@link ConfigSessionOptions.onConflict}. */
|
|
58
|
+
type RebaseOrigin = 'base' | 'draft' | 'drop';
|
|
59
|
+
interface ConfigSessionOptions {
|
|
60
|
+
source?: ConfigSource;
|
|
61
|
+
/** The shared draft. Without one, the pending patches are this client's alone. */
|
|
62
|
+
draft?: DraftSource;
|
|
63
|
+
sink?: ConfigSink;
|
|
64
|
+
/**
|
|
65
|
+
* When to hand the pending patches to the sink — transport, not meaning: how many Yjs updates a
|
|
66
|
+
* burst of edits produces, never which edits belong together (that is the changeset on the stamp).
|
|
67
|
+
* `'microtask'` (the default) flushes at the end of the current synchronous run, which is exactly
|
|
68
|
+
* the boundary a loop of `apply` calls sits inside. A number debounces by that many ms; `'manual'`
|
|
69
|
+
* flushes only on `flush()`.
|
|
70
|
+
*/
|
|
71
|
+
flush?: 'microtask' | 'manual' | number;
|
|
72
|
+
/**
|
|
73
|
+
* Called when a rebase drops local edits the new base invalidated — never silent, because the
|
|
74
|
+
* user made those edits and they are now gone.
|
|
75
|
+
*
|
|
76
|
+
* `origin` says which replay dropped them, because the honest wording differs: `'base'` is a real
|
|
77
|
+
* base move (the classic rebase), `'draft'` is the shared draft being re-adopted onto the same
|
|
78
|
+
* base (a stale or conflicting entry another writer left there), `'drop'` is a dependent casualty
|
|
79
|
+
* of an explicit drop. Each casualty is announced once per life in the draft — the draft replays
|
|
80
|
+
* wholesale on every change, and re-announcing the same doomed entry on every keystroke taught
|
|
81
|
+
* users to ignore the report. A base move resets that memory: the same edit dying on a new base
|
|
82
|
+
* is news.
|
|
83
|
+
*/
|
|
84
|
+
onConflict?: (dropped: RebaseConflict[], origin: RebaseOrigin) => void;
|
|
85
|
+
/**
|
|
86
|
+
* Called ONCE when the shared draft carries a patch stamped at a NEWER schema version than this
|
|
87
|
+
* build understands — a collaborator on a newer deploy is writing to the same draft. The entry is
|
|
88
|
+
* held out of replay (applying it would fail as a bogus conflict) and never retracted (it is valid
|
|
89
|
+
* for newer readers); the only real remedy is reloading onto the newer deploy, so the surface
|
|
90
|
+
* should say exactly that.
|
|
91
|
+
*/
|
|
92
|
+
onSchemaAhead?: () => void;
|
|
93
|
+
/** Test seam — the adopt-side patch upgrade walks this chain/target instead of the registered
|
|
94
|
+
* ones. Real sessions omit it: registration refuses synthetic versions, so only a seam can
|
|
95
|
+
* exercise the walk. */
|
|
96
|
+
schemaWalk?: WalkOptions;
|
|
97
|
+
/** Called when a hooked derivation throws. The surface waiting on that value is now showing
|
|
98
|
+
* something stale, and only the caller can say so. */
|
|
99
|
+
onDeriveError?: (name: string, error: unknown) => void;
|
|
100
|
+
/**
|
|
101
|
+
* Run over every value this session advances to, before anyone reads it — for content that is a
|
|
102
|
+
* FUNCTION of the config rather than an edit to it.
|
|
103
|
+
*
|
|
104
|
+
* The rows a linked system brings are the case: a link declares a pin and the rows are derived from
|
|
105
|
+
* it, so a surface reading the session has to see them, and a replay has to make them again. Purely
|
|
106
|
+
* derived content only. It must add no patches and must not depend on the value it produced, since
|
|
107
|
+
* it runs on the result of every apply, rebase, adopt and drop.
|
|
108
|
+
*
|
|
109
|
+
* The BASE is untouched, which is what makes it safe to run here: a rebase and a drop both replay
|
|
110
|
+
* from the base, so derived content is rebuilt rather than replayed as though someone had written
|
|
111
|
+
* it.
|
|
112
|
+
*/
|
|
113
|
+
derive?: <C extends Config>(config: C) => C;
|
|
114
|
+
}
|
|
115
|
+
declare class ConfigSession<C extends Config = Config> implements LiveConfig {
|
|
116
|
+
#private;
|
|
117
|
+
constructor(config: C, options?: ConfigSessionOptions);
|
|
118
|
+
get config(): C;
|
|
119
|
+
/** Apply a patch and advance to the result. Returns the new config for a caller that wants it
|
|
120
|
+
* immediately; `session.config` is the same value. */
|
|
121
|
+
apply(patch: Patch, id?: string): C;
|
|
122
|
+
/**
|
|
123
|
+
* Move the session forward to a config someone else derived from this one.
|
|
124
|
+
*
|
|
125
|
+
* The seam for code that is handed the next config rather than producing it — an operation
|
|
126
|
+
* handler is `config.apply(...)`, so what it returns is the next value and the session takes it
|
|
127
|
+
* rather than re-deriving it. Patches the value gained on the way join the outgoing batch, so a
|
|
128
|
+
* mutation made this way persists like any other.
|
|
129
|
+
*/
|
|
130
|
+
advance(next: C): C;
|
|
131
|
+
/**
|
|
132
|
+
* Attach one resolved source and advance to the result.
|
|
133
|
+
*
|
|
134
|
+
* A read arriving late, not an edit: the answer is applied to the value the session holds now, so a
|
|
135
|
+
* caller never hands over a config it read a round trip ago, and nothing joins the outgoing batch.
|
|
136
|
+
* The edition is unchanged, which is what lets a write in flight tell this apart from a peer's edit.
|
|
137
|
+
*/
|
|
138
|
+
attach(resolution: Parameters<C['attachResolved']>[0]): C;
|
|
139
|
+
subscribe(listener: () => void): () => void;
|
|
140
|
+
/**
|
|
141
|
+
* Derive something from this config for as long as the caller is interested.
|
|
142
|
+
*
|
|
143
|
+
* A surface hooks what it is showing, which is the point: today a provider subscribes once and
|
|
144
|
+
* every tab pays for the derive whether a canvas is mounted or not. The returned disposer is the
|
|
145
|
+
* whole teardown, so the effect that registers it is one line — and the same point can be hooked
|
|
146
|
+
* twice under different names, so an inspector can watch timings without displacing the paint.
|
|
147
|
+
*
|
|
148
|
+
* ```ts
|
|
149
|
+
* useEffect(() => session.hook('onChange', { styles: stylesOnChange }).dispose, [session]);
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* The lifecycle is named rather than implied so the other points (`onLoad`, `onBuild`, `onPush`)
|
|
153
|
+
* land beside it instead of each inventing a method.
|
|
154
|
+
*/
|
|
155
|
+
hook<H extends ChangeHookMap<C>>(_lifecycle: 'onChange', hooks: H): ChangeHookRun<H>;
|
|
156
|
+
/**
|
|
157
|
+
* The current value of a hooked derivation, by name — the pull half of `hook`.
|
|
158
|
+
*
|
|
159
|
+
* Typed by the caller, because a session accumulates hook sets from surfaces it knows nothing
|
|
160
|
+
* about; the statically-typed read is the `derived` on what `hook` returned. Two hook sets may
|
|
161
|
+
* claim one name — an inspector watching the same point — and this answers with the first that has
|
|
162
|
+
* answered, so a reader that cares which is reading the wrong way round.
|
|
163
|
+
*/
|
|
164
|
+
derived<T>(name: string): T | undefined;
|
|
165
|
+
/** Replay the local edits onto a new base. Dropped edits go to `onConflict`. */
|
|
166
|
+
rebase(base: Snapshot): void;
|
|
167
|
+
/** Drop local edits a merge folded into the base. */
|
|
168
|
+
merge(patchIds: readonly string[]): void;
|
|
169
|
+
/**
|
|
170
|
+
* Drop local edits the server refused — the opposite of `merge`, and the difference matters.
|
|
171
|
+
*
|
|
172
|
+
* A merged patch leaves its value readable, because the new base contains it. A refused one must
|
|
173
|
+
* take its effect with it: the edit was not saved, so a surface that kept showing it would be
|
|
174
|
+
* lying about what the branch holds. That is why this replays the remaining draft instead of
|
|
175
|
+
* filtering the pending list — a later patch built on the refused one has nothing to build on, and
|
|
176
|
+
* the replay is what surfaces that rather than leaving a half-applied overlay.
|
|
177
|
+
*
|
|
178
|
+
* Those dependent casualties go to `onConflict` like any other dropped edit. Unknown ids change
|
|
179
|
+
* nothing, so a retry that already settled is harmless.
|
|
180
|
+
*
|
|
181
|
+
* The removal is also written through to the sink, for every dropped patch that had reached it. It
|
|
182
|
+
* has to be: the pending list this filters is local, and a client adopts the shared draft wholesale,
|
|
183
|
+
* so a patch removed only here reappears on the next change notification — any peer edit, any
|
|
184
|
+
* reconnect. See {@link ConfigSink.retract}.
|
|
185
|
+
*/
|
|
186
|
+
drop(patchIds: readonly string[]): void;
|
|
187
|
+
/** Hand the pending batch to the sink now. Resolves once the sink has taken it. */
|
|
188
|
+
flush(): Promise<void>;
|
|
189
|
+
/** Detach from the source. The config value remains readable — it just stops advancing. */
|
|
190
|
+
dispose(): void;
|
|
191
|
+
}
|
|
192
|
+
/** A source over a fixed snapshot that never changes — the in-memory case, and what
|
|
193
|
+
* `MapItemStore` used to be. Useful as a seed and in tests. */
|
|
194
|
+
declare function memoryConfigSource(snapshot: Snapshot): ConfigSource;
|
|
195
|
+
//#endregion
|
|
196
|
+
export { ConfigSession, ConfigSessionOptions, ConfigSink, ConfigSource, DraftSource, RebaseOrigin, SourceChange, memoryConfigSource };
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { upgradeDraftEntries } from "./schema-version.js";
|
|
2
|
+
import { runChangeHooks } from "./change-hooks.js";
|
|
3
|
+
//#region src/framework/session.ts
|
|
4
|
+
/**
|
|
5
|
+
* Whether two patch lists are the same draft — same entries, same order.
|
|
6
|
+
*
|
|
7
|
+
* By ID, because an entry is immutable once stamped: the id is minted with the patch and travels with
|
|
8
|
+
* it through the sink, the shared list and the storage row, so two entries sharing one are the same
|
|
9
|
+
* edit however many times it was serialized on the way round.
|
|
10
|
+
*/
|
|
11
|
+
function sameDraft(a, b) {
|
|
12
|
+
return a.length === b.length && a.every((entry, i) => entry.id === b[i]?.id);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One entry per id, first occurrence kept, order otherwise preserved.
|
|
16
|
+
*
|
|
17
|
+
* A shared draft can carry the same stamped patch twice: two realtime instances that each seeded a
|
|
18
|
+
* cold room from the same rows mint their own Yjs items for one id, and the merge keeps both. The
|
|
19
|
+
* second copy is the same edit, so it can't be replayed — a `create` fails as `ALREADY_EXISTS` — and
|
|
20
|
+
* reporting it would tell the user an edit was lost when nothing was.
|
|
21
|
+
*/
|
|
22
|
+
function uniqueById(entries) {
|
|
23
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24
|
+
const unique = [];
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (seen.has(entry.id)) continue;
|
|
27
|
+
seen.add(entry.id);
|
|
28
|
+
unique.push(entry);
|
|
29
|
+
}
|
|
30
|
+
return unique;
|
|
31
|
+
}
|
|
32
|
+
var ConfigSession = class {
|
|
33
|
+
#config;
|
|
34
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
35
|
+
#options;
|
|
36
|
+
/** Applied locally but not yet handed to the sink. Distinct from `config.pending`, which stays
|
|
37
|
+
* populated until the source confirms — sent and confirmed are different questions. */
|
|
38
|
+
#uncommitted = [];
|
|
39
|
+
/** Casualties already reported to `onConflict`, by id — the once-per-life memory. Pruned to the
|
|
40
|
+
* shared draft on every adopt, cleared whenever the base actually moves. */
|
|
41
|
+
#announcedDrops = /* @__PURE__ */ new Set();
|
|
42
|
+
/** Whether {@link ConfigSessionOptions.onSchemaAhead} already fired — the draft re-adopts
|
|
43
|
+
* wholesale on every change, and one reload prompt is the message. */
|
|
44
|
+
#schemaAheadAnnounced = false;
|
|
45
|
+
/**
|
|
46
|
+
* Patches handed to the sink whose `commit` hasn't resolved yet, in apply order.
|
|
47
|
+
*
|
|
48
|
+
* They are in neither `#uncommitted` (flush empties it before awaiting) nor durably shared, and that
|
|
49
|
+
* gap matters twice:
|
|
50
|
+
*
|
|
51
|
+
* - for `drop`, retracting one now would find nothing to remove, and the append would land
|
|
52
|
+
* afterwards — resurrecting the patch permanently. So a drop naming one of these defers.
|
|
53
|
+
* - for {@link #adoptDraft}, which rebuilds the pending list from the shared draft plus whatever
|
|
54
|
+
* is only local. A patch in neither list is not dropped, it simply stops existing — no replay,
|
|
55
|
+
* no conflict, no report. That erased "add a token, then delete it while the add is still
|
|
56
|
+
* committing", leaving the delete to replay alone and fail as `NOT_FOUND`.
|
|
57
|
+
*
|
|
58
|
+
* So this holds the entries rather than their ids: the adopt needs the patch to replay and the
|
|
59
|
+
* position to replay it at, and a `Set<string>` had neither.
|
|
60
|
+
*/
|
|
61
|
+
#inFlight = [];
|
|
62
|
+
/** Ids dropped while their own commit was in flight — retracted once it settles. */
|
|
63
|
+
#retractOnSettle = /* @__PURE__ */ new Set();
|
|
64
|
+
/** Derivations hooked onto this session — held so `dispose` takes them down with it. */
|
|
65
|
+
#runs = /* @__PURE__ */ new Set();
|
|
66
|
+
#scheduled = false;
|
|
67
|
+
#timer;
|
|
68
|
+
#unsubscribe;
|
|
69
|
+
#unsubscribeDraft;
|
|
70
|
+
constructor(config, options = {}) {
|
|
71
|
+
this.#options = options;
|
|
72
|
+
this.#config = this.#derived(config.withOwner(this));
|
|
73
|
+
if (options.source) {
|
|
74
|
+
this.#config = this.#afterRebase(this.#config.rebase(options.source.snapshot()), "base");
|
|
75
|
+
this.#unsubscribe = options.source.subscribe((change) => this.#onSourceChange(change));
|
|
76
|
+
}
|
|
77
|
+
if (options.draft) {
|
|
78
|
+
this.#adoptDraft(options.draft.draft());
|
|
79
|
+
this.#unsubscribeDraft = options.draft.subscribe((draft) => {
|
|
80
|
+
if (this.#adoptDraft(draft)) this.#notify();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
get config() {
|
|
85
|
+
return this.#config;
|
|
86
|
+
}
|
|
87
|
+
/** Apply a patch and advance to the result. Returns the new config for a caller that wants it
|
|
88
|
+
* immediately; `session.config` is the same value. */
|
|
89
|
+
apply(patch, id) {
|
|
90
|
+
return this.advance(this.#config.apply(patch, id));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Move the session forward to a config someone else derived from this one.
|
|
94
|
+
*
|
|
95
|
+
* The seam for code that is handed the next config rather than producing it — an operation
|
|
96
|
+
* handler is `config.apply(...)`, so what it returns is the next value and the session takes it
|
|
97
|
+
* rather than re-deriving it. Patches the value gained on the way join the outgoing batch, so a
|
|
98
|
+
* mutation made this way persists like any other.
|
|
99
|
+
*/
|
|
100
|
+
advance(next) {
|
|
101
|
+
if (next === this.#config) return next;
|
|
102
|
+
const before = this.#config;
|
|
103
|
+
this.#config = this.#derived(next);
|
|
104
|
+
if (next.pending.length > before.pending.length) {
|
|
105
|
+
this.#uncommitted.push(...next.pending.slice(before.pending.length));
|
|
106
|
+
this.#schedule();
|
|
107
|
+
}
|
|
108
|
+
this.#notify();
|
|
109
|
+
return this.#config;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Attach one resolved source and advance to the result.
|
|
113
|
+
*
|
|
114
|
+
* A read arriving late, not an edit: the answer is applied to the value the session holds now, so a
|
|
115
|
+
* caller never hands over a config it read a round trip ago, and nothing joins the outgoing batch.
|
|
116
|
+
* The edition is unchanged, which is what lets a write in flight tell this apart from a peer's edit.
|
|
117
|
+
*/
|
|
118
|
+
attach(resolution) {
|
|
119
|
+
this.#config = this.#derived(this.#config.attachResolved(resolution));
|
|
120
|
+
this.#notify();
|
|
121
|
+
return this.#config;
|
|
122
|
+
}
|
|
123
|
+
subscribe(listener) {
|
|
124
|
+
this.#listeners.add(listener);
|
|
125
|
+
return () => this.#listeners.delete(listener);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Derive something from this config for as long as the caller is interested.
|
|
129
|
+
*
|
|
130
|
+
* A surface hooks what it is showing, which is the point: today a provider subscribes once and
|
|
131
|
+
* every tab pays for the derive whether a canvas is mounted or not. The returned disposer is the
|
|
132
|
+
* whole teardown, so the effect that registers it is one line — and the same point can be hooked
|
|
133
|
+
* twice under different names, so an inspector can watch timings without displacing the paint.
|
|
134
|
+
*
|
|
135
|
+
* ```ts
|
|
136
|
+
* useEffect(() => session.hook('onChange', { styles: stylesOnChange }).dispose, [session]);
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* The lifecycle is named rather than implied so the other points (`onLoad`, `onBuild`, `onPush`)
|
|
140
|
+
* land beside it instead of each inventing a method.
|
|
141
|
+
*/
|
|
142
|
+
hook(_lifecycle, hooks) {
|
|
143
|
+
const run = runChangeHooks({
|
|
144
|
+
feed: {
|
|
145
|
+
current: () => this.#config,
|
|
146
|
+
subscribe: (listener) => this.subscribe(listener)
|
|
147
|
+
},
|
|
148
|
+
hooks,
|
|
149
|
+
onError: this.#options.onDeriveError
|
|
150
|
+
});
|
|
151
|
+
const registered = {
|
|
152
|
+
...run,
|
|
153
|
+
dispose: () => {
|
|
154
|
+
this.#runs.delete(registered);
|
|
155
|
+
run.dispose();
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
this.#runs.add(registered);
|
|
159
|
+
return registered;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The current value of a hooked derivation, by name — the pull half of `hook`.
|
|
163
|
+
*
|
|
164
|
+
* Typed by the caller, because a session accumulates hook sets from surfaces it knows nothing
|
|
165
|
+
* about; the statically-typed read is the `derived` on what `hook` returned. Two hook sets may
|
|
166
|
+
* claim one name — an inspector watching the same point — and this answers with the first that has
|
|
167
|
+
* answered, so a reader that cares which is reading the wrong way round.
|
|
168
|
+
*/
|
|
169
|
+
derived(name) {
|
|
170
|
+
for (const run of this.#runs) {
|
|
171
|
+
const value = run.derived(name);
|
|
172
|
+
if (value !== void 0) return value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Replay the local edits onto a new base. Dropped edits go to `onConflict`. */
|
|
176
|
+
rebase(base) {
|
|
177
|
+
this.#config = this.#afterRebase(this.#config.rebase(base), "base");
|
|
178
|
+
this.#notify();
|
|
179
|
+
}
|
|
180
|
+
/** Drop local edits a merge folded into the base. */
|
|
181
|
+
merge(patchIds) {
|
|
182
|
+
const next = this.#config.merge(patchIds);
|
|
183
|
+
if (next === this.#config) return;
|
|
184
|
+
this.#config = this.#derived(next);
|
|
185
|
+
this.#notify();
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Drop local edits the server refused — the opposite of `merge`, and the difference matters.
|
|
189
|
+
*
|
|
190
|
+
* A merged patch leaves its value readable, because the new base contains it. A refused one must
|
|
191
|
+
* take its effect with it: the edit was not saved, so a surface that kept showing it would be
|
|
192
|
+
* lying about what the branch holds. That is why this replays the remaining draft instead of
|
|
193
|
+
* filtering the pending list — a later patch built on the refused one has nothing to build on, and
|
|
194
|
+
* the replay is what surfaces that rather than leaving a half-applied overlay.
|
|
195
|
+
*
|
|
196
|
+
* Those dependent casualties go to `onConflict` like any other dropped edit. Unknown ids change
|
|
197
|
+
* nothing, so a retry that already settled is harmless.
|
|
198
|
+
*
|
|
199
|
+
* The removal is also written through to the sink, for every dropped patch that had reached it. It
|
|
200
|
+
* has to be: the pending list this filters is local, and a client adopts the shared draft wholesale,
|
|
201
|
+
* so a patch removed only here reappears on the next change notification — any peer edit, any
|
|
202
|
+
* reconnect. See {@link ConfigSink.retract}.
|
|
203
|
+
*/
|
|
204
|
+
drop(patchIds) {
|
|
205
|
+
if (patchIds.length === 0) return;
|
|
206
|
+
const refused = new Set(patchIds);
|
|
207
|
+
const before = this.#config.pending;
|
|
208
|
+
const kept = before.filter((entry) => !refused.has(entry.id) && !(entry.changeset !== void 0 && refused.has(entry.changeset)));
|
|
209
|
+
if (kept.length === before.length) return;
|
|
210
|
+
const neverSent = new Set(this.#uncommitted.map((entry) => entry.id));
|
|
211
|
+
this.#uncommitted = this.#uncommitted.filter((entry) => !refused.has(entry.id));
|
|
212
|
+
this.#config = this.#afterRebase(this.#config.withDraft(kept), "drop");
|
|
213
|
+
this.#notify();
|
|
214
|
+
const survived = new Set(this.#config.pending.map((entry) => entry.id));
|
|
215
|
+
this.#retract(before.map((entry) => entry.id).filter((id) => !survived.has(id) && !neverSent.has(id)));
|
|
216
|
+
}
|
|
217
|
+
/** Write a removal through to the sink, deferring any id whose own commit is still in flight. */
|
|
218
|
+
#retract(ids) {
|
|
219
|
+
if (ids.length === 0) return;
|
|
220
|
+
const inFlight = new Set(this.#inFlight.map((entry) => entry.id));
|
|
221
|
+
const now = [];
|
|
222
|
+
for (const id of ids) if (inFlight.has(id)) this.#retractOnSettle.add(id);
|
|
223
|
+
else now.push(id);
|
|
224
|
+
this.#sendRetraction(now);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Hand ids to the sink's `retract`, if it has one.
|
|
228
|
+
*
|
|
229
|
+
* A sink without it keeps `drop` local-only — the behaviour before the pair existed — rather than
|
|
230
|
+
* throwing. A rejected retraction goes back on the deferred set so the next settle retries it:
|
|
231
|
+
* losing one strands the row in the shared draft, which is the resurrection this exists to stop.
|
|
232
|
+
* Deliberately not a background retry loop — that is a client-side sweeper, and a sweeper is where
|
|
233
|
+
* full-set pushes come from.
|
|
234
|
+
*/
|
|
235
|
+
#sendRetraction(ids) {
|
|
236
|
+
if (ids.length === 0) return;
|
|
237
|
+
const retract = this.#options.sink?.retract;
|
|
238
|
+
if (!retract) return;
|
|
239
|
+
try {
|
|
240
|
+
const settled = retract.call(this.#options.sink, ids);
|
|
241
|
+
if (settled) settled.catch(() => {
|
|
242
|
+
for (const id of ids) this.#retractOnSettle.add(id);
|
|
243
|
+
});
|
|
244
|
+
} catch {
|
|
245
|
+
for (const id of ids) this.#retractOnSettle.add(id);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** Hand the pending batch to the sink now. Resolves once the sink has taken it. */
|
|
249
|
+
async flush() {
|
|
250
|
+
this.#scheduled = false;
|
|
251
|
+
if (this.#timer !== void 0) {
|
|
252
|
+
clearTimeout(this.#timer);
|
|
253
|
+
this.#timer = void 0;
|
|
254
|
+
}
|
|
255
|
+
const batch = this.#uncommitted;
|
|
256
|
+
if (batch.length === 0 || !this.#options.sink) return;
|
|
257
|
+
this.#uncommitted = [];
|
|
258
|
+
this.#inFlight = [...this.#inFlight, ...batch];
|
|
259
|
+
try {
|
|
260
|
+
await this.#options.sink.commit(batch, this.#config);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
this.#uncommitted = [...batch.filter((entry) => !this.#retractOnSettle.has(entry.id)), ...this.#uncommitted];
|
|
263
|
+
throw error;
|
|
264
|
+
} finally {
|
|
265
|
+
const settled = new Set(batch.map((entry) => entry.id));
|
|
266
|
+
this.#inFlight = this.#inFlight.filter((entry) => !settled.has(entry.id));
|
|
267
|
+
const deferred = [...this.#retractOnSettle];
|
|
268
|
+
this.#retractOnSettle.clear();
|
|
269
|
+
this.#sendRetraction(deferred);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/** Detach from the source. The config value remains readable — it just stops advancing. */
|
|
273
|
+
dispose() {
|
|
274
|
+
this.#unsubscribe?.();
|
|
275
|
+
this.#unsubscribe = void 0;
|
|
276
|
+
this.#unsubscribeDraft?.();
|
|
277
|
+
this.#unsubscribeDraft = void 0;
|
|
278
|
+
for (const run of [...this.#runs]) run.dispose();
|
|
279
|
+
this.#listeners.clear();
|
|
280
|
+
if (this.#timer !== void 0) clearTimeout(this.#timer);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Take the shared draft as the pending list. Answers whether the config moved, so a change that
|
|
284
|
+
* says nothing new doesn't advance.
|
|
285
|
+
*
|
|
286
|
+
* Local patches that haven't reached the shared list yet are kept on the end. A batch applied a
|
|
287
|
+
* microtask ago is ours and still uncommitted, and adopting a list that predates it would undo
|
|
288
|
+
* the user's last edit the moment a collaborator typed.
|
|
289
|
+
*
|
|
290
|
+
* "Local" is in flight plus uncommitted, in that order, because those are the two ways a patch can
|
|
291
|
+
* be ours and not yet shared — and a patch in neither list doesn't get reported as dropped, it just
|
|
292
|
+
* stops existing. Reading only `#uncommitted` erased the whole in-flight batch: add a token, delete
|
|
293
|
+
* it before the add's commit settles, and the add vanished while the delete replayed alone onto a
|
|
294
|
+
* base that never had the token, dying as `NOT_FOUND`. In flight comes first because it was applied
|
|
295
|
+
* first — `flush` takes the head of the queue.
|
|
296
|
+
*
|
|
297
|
+
* An in-flight entry a `drop` already claimed is left out: `drop` filters `#uncommitted` itself but
|
|
298
|
+
* cannot touch a sent batch (it defers the retraction to `#retractOnSettle`), so without this the
|
|
299
|
+
* adopt would read the dropped patch straight back.
|
|
300
|
+
*/
|
|
301
|
+
#adoptDraft(draft) {
|
|
302
|
+
const { entries: readable, held } = upgradeDraftEntries(draft, this.#options.schemaWalk);
|
|
303
|
+
if (held.length > 0 && !this.#schemaAheadAnnounced) {
|
|
304
|
+
this.#schemaAheadAnnounced = true;
|
|
305
|
+
this.#options.onSchemaAhead?.();
|
|
306
|
+
}
|
|
307
|
+
const shared = new Set(readable.map((entry) => entry.id));
|
|
308
|
+
const localOnly = [...this.#inFlight, ...this.#uncommitted].filter((entry) => !shared.has(entry.id) && !this.#retractOnSettle.has(entry.id));
|
|
309
|
+
for (const id of this.#announcedDrops) if (!shared.has(id)) this.#announcedDrops.delete(id);
|
|
310
|
+
const adopted = uniqueById([...readable, ...localOnly]);
|
|
311
|
+
if (sameDraft(adopted, this.#config.pending)) return false;
|
|
312
|
+
this.#config = this.#afterRebase(this.#config.withDraft(adopted), "draft");
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
#onSourceChange(change) {
|
|
316
|
+
let next = this.#config;
|
|
317
|
+
if (change.merged?.length) next = next.merge(change.merged);
|
|
318
|
+
this.#config = this.#afterRebase(next.rebase(change.snapshot), "base");
|
|
319
|
+
this.#notify();
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Unwrap a rebase result, reporting whatever it had to drop — a local edit the new base
|
|
323
|
+
* invalidated is the user's work disappearing, so it is never silent.
|
|
324
|
+
*
|
|
325
|
+
* Announced once per casualty: the shared draft replays wholesale on every change, so without a
|
|
326
|
+
* memory the same doomed entry re-reported on every subsequent edit, its own echo included — an
|
|
327
|
+
* alarm that fires forever is an alarm nobody reads. A base move clears the memory (the same edit
|
|
328
|
+
* dying on a new base is news); an id that leaves the draft is forgotten in {@link #adoptDraft}.
|
|
329
|
+
*/
|
|
330
|
+
#afterRebase({ config, dropped }, origin) {
|
|
331
|
+
if (origin === "base") this.#announcedDrops.clear();
|
|
332
|
+
const news = dropped.filter((entry) => !this.#announcedDrops.has(entry.id));
|
|
333
|
+
for (const entry of news) this.#announcedDrops.add(entry.id);
|
|
334
|
+
if (news.length) this.#options.onConflict?.(news, origin);
|
|
335
|
+
return this.#derived(config);
|
|
336
|
+
}
|
|
337
|
+
/** One value, ready to be read — {@link ConfigSessionOptions.derive} applied, or the value itself
|
|
338
|
+
* when the session was given none. Every assignment to `#config` goes through here. */
|
|
339
|
+
#derived(config) {
|
|
340
|
+
return this.#options.derive ? this.#options.derive(config) : config;
|
|
341
|
+
}
|
|
342
|
+
#schedule() {
|
|
343
|
+
const { flush = "microtask", sink } = this.#options;
|
|
344
|
+
if (!sink || flush === "manual" || this.#scheduled) return;
|
|
345
|
+
this.#scheduled = true;
|
|
346
|
+
if (flush === "microtask") {
|
|
347
|
+
queueMicrotask(() => {
|
|
348
|
+
if (this.#scheduled) this.flush();
|
|
349
|
+
});
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
this.#timer = setTimeout(() => void this.flush(), flush);
|
|
353
|
+
}
|
|
354
|
+
#notify() {
|
|
355
|
+
for (const listener of this.#listeners) listener();
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
/** A source over a fixed snapshot that never changes — the in-memory case, and what
|
|
359
|
+
* `MapItemStore` used to be. Useful as a seed and in tests. */
|
|
360
|
+
function memoryConfigSource(snapshot) {
|
|
361
|
+
return {
|
|
362
|
+
snapshot: () => snapshot,
|
|
363
|
+
subscribe: () => () => {}
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
//#endregion
|
|
367
|
+
export { ConfigSession, memoryConfigSource };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/framework/signature.d.ts
|
|
4
|
+
interface SignatureOptions {
|
|
5
|
+
/** Object nesting rendered in full before an object collapses to `{…}`. */
|
|
6
|
+
readonly depth?: number;
|
|
7
|
+
/** Enum members listed before the rest collapse to `…`. */
|
|
8
|
+
readonly enumLimit?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Render one JSON Schema node as a type signature. `root` holds `$defs` for `$ref` lookups. */
|
|
11
|
+
declare function renderSignature(schema: unknown, options?: SignatureOptions): string;
|
|
12
|
+
/** The signature of a zod schema, read from its input side since it documents what an author writes. */
|
|
13
|
+
declare function signatureOf(schema: z.ZodTypeAny | undefined, options?: SignatureOptions): string | undefined;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { SignatureOptions, renderSignature, signatureOf };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/framework/signature.ts
|
|
3
|
+
/**
|
|
4
|
+
* A JSON Schema rendered as a one-line type signature — `{ value: string | ref<token>, type?: string }`.
|
|
5
|
+
*
|
|
6
|
+
* The full JSON Schema for one create runs to tens of kilobytes; the signature says the same shape in
|
|
7
|
+
* a line a prompt can afford to carry for every kind, so a model learns what an entity takes before
|
|
8
|
+
* it writes one. Depth is capped: an object nested past it prints as `{…}`, and the full schema is one
|
|
9
|
+
* `address` describe away. Two levels is enough to show a kind's fields and the shape of each field's
|
|
10
|
+
* members; a token's color operation is recursive and would otherwise unfold without end.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_DEPTH = 2;
|
|
13
|
+
const DEFAULT_ENUM_LIMIT = 8;
|
|
14
|
+
function isNode(value) {
|
|
15
|
+
return typeof value === "object" && value !== null;
|
|
16
|
+
}
|
|
17
|
+
function literal(value) {
|
|
18
|
+
return typeof value === "string" ? `'${value}'` : String(value);
|
|
19
|
+
}
|
|
20
|
+
function unique(parts) {
|
|
21
|
+
return [...new Set(parts)];
|
|
22
|
+
}
|
|
23
|
+
/** Render one JSON Schema node as a type signature. `root` holds `$defs` for `$ref` lookups. */
|
|
24
|
+
function renderSignature(schema, options = {}) {
|
|
25
|
+
if (!isNode(schema)) return "unknown";
|
|
26
|
+
const depth = options.depth ?? DEFAULT_DEPTH;
|
|
27
|
+
const enumLimit = options.enumLimit ?? DEFAULT_ENUM_LIMIT;
|
|
28
|
+
const render = (node, remaining) => {
|
|
29
|
+
if (node.$ref) {
|
|
30
|
+
const name = node.$ref.replace("#/$defs/", "");
|
|
31
|
+
const target = schema.$defs?.[name];
|
|
32
|
+
return target ? render(target, remaining) : "…";
|
|
33
|
+
}
|
|
34
|
+
if (node.ref && node.refKind) return `ref<${node.refKind}>`;
|
|
35
|
+
if (node.const !== void 0) return literal(node.const);
|
|
36
|
+
if (node.enum) {
|
|
37
|
+
const shown = node.enum.slice(0, enumLimit).map(literal);
|
|
38
|
+
if (node.enum.length > enumLimit) shown.push("…");
|
|
39
|
+
return shown.join(" | ");
|
|
40
|
+
}
|
|
41
|
+
const variants = node.anyOf ?? node.oneOf;
|
|
42
|
+
if (variants) return unique(variants.map((member) => render(member, remaining))).join(" | ");
|
|
43
|
+
if (node.allOf) return unique(node.allOf.map((member) => render(member, remaining))).join(" & ");
|
|
44
|
+
if (Array.isArray(node.type)) return unique(node.type.map((type) => render({
|
|
45
|
+
...node,
|
|
46
|
+
type
|
|
47
|
+
}, remaining))).join(" | ");
|
|
48
|
+
if (node.type === "array") {
|
|
49
|
+
const item = node.items ? render(node.items, remaining) : "unknown";
|
|
50
|
+
return item.includes(" | ") ? `(${item})[]` : `${item}[]`;
|
|
51
|
+
}
|
|
52
|
+
if (node.type === "object" || node.properties || node.additionalProperties) return renderObject(node, remaining);
|
|
53
|
+
return typeof node.type === "string" ? node.type : "unknown";
|
|
54
|
+
};
|
|
55
|
+
const renderObject = (node, remaining) => {
|
|
56
|
+
const entries = Object.entries(node.properties ?? {});
|
|
57
|
+
if (entries.length === 1 && entries[0]?.[0] === "__ref") return "ref";
|
|
58
|
+
if (entries.length === 0) {
|
|
59
|
+
if (isNode(node.additionalProperties)) {
|
|
60
|
+
if (remaining <= 0) return "Record<string, {…}>";
|
|
61
|
+
return `Record<string, ${render(node.additionalProperties, remaining - 1)}>`;
|
|
62
|
+
}
|
|
63
|
+
return node.additionalProperties === false ? "{}" : "Record<string, unknown>";
|
|
64
|
+
}
|
|
65
|
+
if (remaining <= 0) return "{…}";
|
|
66
|
+
const required = new Set(node.required ?? []);
|
|
67
|
+
return `{ ${entries.map(([key, value]) => {
|
|
68
|
+
return `${key}${required.has(key) ? "" : "?"}: ${render(value, remaining - 1)}`;
|
|
69
|
+
}).join(", ")} }`;
|
|
70
|
+
};
|
|
71
|
+
return render(schema, depth);
|
|
72
|
+
}
|
|
73
|
+
/** The signature of a zod schema, read from its input side since it documents what an author writes. */
|
|
74
|
+
function signatureOf(schema, options) {
|
|
75
|
+
if (!schema) return void 0;
|
|
76
|
+
try {
|
|
77
|
+
return renderSignature(z.toJSONSchema(schema, { io: "input" }), options);
|
|
78
|
+
} catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
export { renderSignature, signatureOf };
|