foldkit-primitives 0.15.3 → 0.16.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/dist/device/fullscreen.d.ts +28 -0
- package/dist/device/fullscreen.d.ts.map +1 -0
- package/dist/device/fullscreen.js +69 -0
- package/dist/device/fullscreen.js.map +1 -0
- package/dist/device/geolocation.d.ts +56 -0
- package/dist/device/geolocation.d.ts.map +1 -0
- package/dist/device/geolocation.js +87 -0
- package/dist/device/geolocation.js.map +1 -0
- package/dist/device/index.d.ts +5 -0
- package/dist/device/index.js +6 -0
- package/dist/device/media-devices.d.ts +88 -0
- package/dist/device/media-devices.d.ts.map +1 -0
- package/dist/device/media-devices.js +117 -0
- package/dist/device/media-devices.js.map +1 -0
- package/dist/device/media-stream.d.ts +93 -0
- package/dist/device/media-stream.d.ts.map +1 -0
- package/dist/device/media-stream.js +106 -0
- package/dist/device/media-stream.js.map +1 -0
- package/dist/device/permissions.d.ts +106 -0
- package/dist/device/permissions.d.ts.map +1 -0
- package/dist/device/permissions.js +143 -0
- package/dist/device/permissions.js.map +1 -0
- package/dist/dom/autofocus.d.ts +15 -0
- package/dist/dom/autofocus.d.ts.map +1 -0
- package/dist/dom/autofocus.js +22 -0
- package/dist/dom/autofocus.js.map +1 -0
- package/dist/dom/clipboard.d.ts +30 -0
- package/dist/dom/clipboard.d.ts.map +1 -0
- package/dist/dom/clipboard.js +50 -0
- package/dist/dom/clipboard.js.map +1 -0
- package/dist/dom/editable-text.d.ts +72 -0
- package/dist/dom/editable-text.d.ts.map +1 -0
- package/dist/dom/editable-text.js +232 -0
- package/dist/dom/editable-text.js.map +1 -0
- package/dist/dom/fluid-hover.d.ts +93 -0
- package/dist/dom/fluid-hover.d.ts.map +1 -0
- package/dist/dom/fluid-hover.js +294 -0
- package/dist/dom/fluid-hover.js.map +1 -0
- package/dist/dom/focus-scope.d.ts +21 -0
- package/dist/dom/focus-scope.d.ts.map +1 -0
- package/dist/dom/focus-scope.js +93 -0
- package/dist/dom/focus-scope.js.map +1 -0
- package/dist/dom/follow-tab-stop.d.ts +11 -0
- package/dist/dom/follow-tab-stop.d.ts.map +1 -0
- package/dist/dom/follow-tab-stop.js +54 -0
- package/dist/dom/follow-tab-stop.js.map +1 -0
- package/dist/dom/image-decode.d.ts +27 -0
- package/dist/dom/image-decode.d.ts.map +1 -0
- package/dist/dom/image-decode.js +50 -0
- package/dist/dom/image-decode.js.map +1 -0
- package/dist/dom/index.d.ts +24 -0
- package/dist/dom/index.js +25 -0
- package/dist/dom/input-mask.d.ts +35 -0
- package/dist/dom/input-mask.d.ts.map +1 -0
- package/dist/dom/input-mask.js +90 -0
- package/dist/dom/input-mask.js.map +1 -0
- package/dist/dom/keep-in-view.d.ts +16 -0
- package/dist/dom/keep-in-view.d.ts.map +1 -0
- package/dist/dom/keep-in-view.js +38 -0
- package/dist/dom/keep-in-view.js.map +1 -0
- package/dist/dom/keyboard-inset.d.ts +29 -0
- package/dist/dom/keyboard-inset.d.ts.map +1 -0
- package/dist/dom/keyboard-inset.js +45 -0
- package/dist/dom/keyboard-inset.js.map +1 -0
- package/dist/dom/layers.d.ts +6 -0
- package/dist/dom/layers.d.ts.map +1 -0
- package/dist/dom/layers.js +35 -0
- package/dist/dom/layers.js.map +1 -0
- package/dist/dom/measure.d.ts +42 -0
- package/dist/dom/measure.d.ts.map +1 -0
- package/dist/dom/measure.js +123 -0
- package/dist/dom/measure.js.map +1 -0
- package/dist/dom/move.d.ts +45 -0
- package/dist/dom/move.d.ts.map +1 -0
- package/dist/dom/move.js +96 -0
- package/dist/dom/move.js.map +1 -0
- package/dist/dom/pointed-links.d.ts +13 -0
- package/dist/dom/pointed-links.d.ts.map +1 -0
- package/dist/dom/pointed-links.js +59 -0
- package/dist/dom/pointed-links.js.map +1 -0
- package/dist/dom/pointer-drag.d.ts +132 -0
- package/dist/dom/pointer-drag.d.ts.map +1 -0
- package/dist/dom/pointer-drag.js +307 -0
- package/dist/dom/pointer-drag.js.map +1 -0
- package/dist/dom/pointer-position.d.ts +133 -0
- package/dist/dom/pointer-position.d.ts.map +1 -0
- package/dist/dom/pointer-position.js +173 -0
- package/dist/dom/pointer-position.js.map +1 -0
- package/dist/dom/script-loader.d.ts +12 -0
- package/dist/dom/script-loader.d.ts.map +1 -0
- package/dist/dom/script-loader.js +54 -0
- package/dist/dom/script-loader.js.map +1 -0
- package/dist/dom/scroll-bars.d.ts +52 -0
- package/dist/dom/scroll-bars.d.ts.map +1 -0
- package/dist/dom/scroll-bars.js +259 -0
- package/dist/dom/scroll-bars.js.map +1 -0
- package/dist/dom/scroll-settle.d.ts +33 -0
- package/dist/dom/scroll-settle.d.ts.map +1 -0
- package/dist/dom/scroll-settle.js +84 -0
- package/dist/dom/scroll-settle.js.map +1 -0
- package/dist/dom/scroll.d.ts +21 -0
- package/dist/dom/scroll.d.ts.map +1 -0
- package/dist/dom/scroll.js +141 -0
- package/dist/dom/scroll.js.map +1 -0
- package/dist/dom/share.d.ts +19 -0
- package/dist/dom/share.d.ts.map +1 -0
- package/dist/dom/share.js +41 -0
- package/dist/dom/share.js.map +1 -0
- package/dist/dom/swipe.d.ts +121 -0
- package/dist/dom/swipe.d.ts.map +1 -0
- package/dist/dom/swipe.js +291 -0
- package/dist/dom/swipe.js.map +1 -0
- package/dist/dom/targets.d.ts +45 -0
- package/dist/dom/targets.d.ts.map +1 -0
- package/dist/dom/targets.js +70 -0
- package/dist/dom/targets.js.map +1 -0
- package/dist/dom/track-box.d.ts +61 -0
- package/dist/dom/track-box.d.ts.map +1 -0
- package/dist/dom/track-box.js +131 -0
- package/dist/dom/track-box.js.map +1 -0
- package/dist/events/active-element.d.ts +15 -0
- package/dist/events/active-element.d.ts.map +1 -0
- package/dist/events/active-element.js +33 -0
- package/dist/events/active-element.js.map +1 -0
- package/dist/events/hotkeys.d.ts +22 -0
- package/dist/events/hotkeys.d.ts.map +1 -0
- package/dist/events/hotkeys.js +56 -0
- package/dist/events/hotkeys.js.map +1 -0
- package/dist/events/idle.d.ts +29 -0
- package/dist/events/idle.d.ts.map +1 -0
- package/dist/events/idle.js +51 -0
- package/dist/events/idle.js.map +1 -0
- package/dist/events/index.d.ts +9 -0
- package/dist/events/index.js +10 -0
- package/dist/{input-modality-BY-bSrEc.d.mts → events/input-modality.d.ts} +9 -12
- package/dist/events/input-modality.d.ts.map +1 -0
- package/dist/{input-modality-BXD383jg.mjs → events/input-modality.js} +3 -4
- package/dist/events/input-modality.js.map +1 -0
- package/dist/events/keyboard.d.ts +30 -0
- package/dist/events/keyboard.d.ts.map +1 -0
- package/dist/events/keyboard.js +55 -0
- package/dist/events/keyboard.js.map +1 -0
- package/dist/events/pointer.d.ts +15 -0
- package/dist/events/pointer.d.ts.map +1 -0
- package/dist/events/pointer.js +28 -0
- package/dist/events/pointer.js.map +1 -0
- package/dist/events/scroll.d.ts +16 -0
- package/dist/events/scroll.d.ts.map +1 -0
- package/dist/events/scroll.js +29 -0
- package/dist/events/scroll.js.map +1 -0
- package/dist/events/visibility.d.ts +25 -0
- package/dist/events/visibility.d.ts.map +1 -0
- package/dist/events/visibility.js +28 -0
- package/dist/events/visibility.js.map +1 -0
- package/dist/events/window-size.d.ts +30 -0
- package/dist/events/window-size.d.ts.map +1 -0
- package/dist/events/window-size.js +63 -0
- package/dist/events/window-size.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/interaction/bundle-behavior.d.ts +37 -0
- package/dist/interaction/bundle-behavior.d.ts.map +1 -0
- package/dist/interaction/bundle-behavior.js +34 -0
- package/dist/interaction/bundle-behavior.js.map +1 -0
- package/dist/interaction/dismiss-layer.d.ts +100 -0
- package/dist/interaction/dismiss-layer.d.ts.map +1 -0
- package/dist/interaction/dismiss-layer.js +140 -0
- package/dist/interaction/dismiss-layer.js.map +1 -0
- package/dist/interaction/editable-text.d.ts +13 -0
- package/dist/interaction/editable-text.d.ts.map +1 -0
- package/dist/interaction/editable-text.js +17 -0
- package/dist/interaction/editable-text.js.map +1 -0
- package/dist/interaction/focus-scope.d.ts +21 -0
- package/dist/interaction/focus-scope.d.ts.map +1 -0
- package/dist/interaction/focus-scope.js +25 -0
- package/dist/interaction/focus-scope.js.map +1 -0
- package/dist/interaction/focus-visible.d.ts +9 -0
- package/dist/interaction/focus-visible.d.ts.map +1 -0
- package/dist/interaction/focus-visible.js +10 -0
- package/dist/interaction/focus-visible.js.map +1 -0
- package/dist/interaction/grid-navigation.d.ts +73 -0
- package/dist/interaction/grid-navigation.d.ts.map +1 -0
- package/dist/interaction/grid-navigation.js +137 -0
- package/dist/interaction/grid-navigation.js.map +1 -0
- package/dist/interaction/index.d.ts +21 -0
- package/dist/interaction/index.js +21 -0
- package/dist/interaction/layers.d.ts +12 -0
- package/dist/interaction/layers.d.ts.map +1 -0
- package/dist/interaction/layers.js +14 -0
- package/dist/interaction/layers.js.map +1 -0
- package/dist/interaction/list-navigation.d.ts +105 -0
- package/dist/interaction/list-navigation.d.ts.map +1 -0
- package/dist/interaction/list-navigation.js +168 -0
- package/dist/interaction/list-navigation.js.map +1 -0
- package/dist/interaction/live-announce.d.ts +70 -0
- package/dist/interaction/live-announce.d.ts.map +1 -0
- package/dist/interaction/live-announce.js +115 -0
- package/dist/interaction/live-announce.js.map +1 -0
- package/dist/interaction/long-press.d.ts +102 -0
- package/dist/interaction/long-press.d.ts.map +1 -0
- package/dist/interaction/long-press.js +115 -0
- package/dist/interaction/long-press.js.map +1 -0
- package/dist/interaction/move.d.ts +10 -0
- package/dist/interaction/move.d.ts.map +1 -0
- package/dist/interaction/move.js +17 -0
- package/dist/interaction/move.js.map +1 -0
- package/dist/interaction/overlay.d.ts +45 -0
- package/dist/interaction/overlay.d.ts.map +1 -0
- package/dist/interaction/overlay.js +57 -0
- package/dist/interaction/overlay.js.map +1 -0
- package/dist/interaction/placing.d.ts +86 -0
- package/dist/interaction/placing.d.ts.map +1 -0
- package/dist/interaction/placing.js +239 -0
- package/dist/interaction/placing.js.map +1 -0
- package/dist/interaction/pointer-drag.d.ts +23 -0
- package/dist/interaction/pointer-drag.d.ts.map +1 -0
- package/dist/interaction/pointer-drag.js +21 -0
- package/dist/interaction/pointer-drag.js.map +1 -0
- package/dist/interaction/press.d.ts +152 -0
- package/dist/interaction/press.d.ts.map +1 -0
- package/dist/interaction/press.js +229 -0
- package/dist/interaction/press.js.map +1 -0
- package/dist/interaction/roving-tabindex.d.ts +85 -0
- package/dist/interaction/roving-tabindex.d.ts.map +1 -0
- package/dist/interaction/roving-tabindex.js +142 -0
- package/dist/interaction/roving-tabindex.js.map +1 -0
- package/dist/interaction/selection.d.ts +72 -0
- package/dist/interaction/selection.d.ts.map +1 -0
- package/dist/interaction/selection.js +129 -0
- package/dist/interaction/selection.js.map +1 -0
- package/dist/interaction/targets.d.ts +15 -0
- package/dist/interaction/targets.d.ts.map +1 -0
- package/dist/interaction/targets.js +20 -0
- package/dist/interaction/targets.js.map +1 -0
- package/dist/interaction/tree-navigation.d.ts +111 -0
- package/dist/interaction/tree-navigation.d.ts.map +1 -0
- package/dist/interaction/tree-navigation.js +207 -0
- package/dist/interaction/tree-navigation.js.map +1 -0
- package/dist/interaction/typeahead.d.ts +73 -0
- package/dist/interaction/typeahead.d.ts.map +1 -0
- package/dist/interaction/typeahead.js +113 -0
- package/dist/interaction/typeahead.js.map +1 -0
- package/dist/internal.d.ts +16 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/{internal-DP-xLKbT.mjs → internal.js} +8 -7
- package/dist/internal.js.map +1 -0
- package/dist/media/breakpoints.d.ts +37 -0
- package/dist/media/breakpoints.d.ts.map +1 -0
- package/dist/media/breakpoints.js +64 -0
- package/dist/media/breakpoints.js.map +1 -0
- package/dist/media/index.d.ts +3 -0
- package/dist/media/index.js +4 -0
- package/dist/media/mediaquery.d.ts +40 -0
- package/dist/media/mediaquery.d.ts.map +1 -0
- package/dist/media/mediaquery.js +42 -0
- package/dist/media/mediaquery.js.map +1 -0
- package/dist/media/platform.d.ts +30 -0
- package/dist/media/platform.d.ts.map +1 -0
- package/dist/media/platform.js +50 -0
- package/dist/media/platform.js.map +1 -0
- package/dist/motion/index.d.ts +5 -0
- package/dist/motion/index.js +5 -0
- package/dist/motion/motion.d.ts +16 -0
- package/dist/motion/motion.d.ts.map +1 -0
- package/dist/motion/motion.js +25 -0
- package/dist/motion/motion.js.map +1 -0
- package/dist/motion/presence.d.ts +37 -0
- package/dist/motion/presence.d.ts.map +1 -0
- package/dist/motion/presence.js +65 -0
- package/dist/motion/presence.js.map +1 -0
- package/dist/motion/spring.d.ts +50 -0
- package/dist/motion/spring.d.ts.map +1 -0
- package/dist/motion/spring.js +92 -0
- package/dist/motion/spring.js.map +1 -0
- package/dist/motion/tween.d.ts +44 -0
- package/dist/motion/tween.d.ts.map +1 -0
- package/dist/motion/tween.js +65 -0
- package/dist/motion/tween.js.map +1 -0
- package/dist/net/broadcast.d.ts +39 -0
- package/dist/net/broadcast.d.ts.map +1 -0
- package/dist/net/broadcast.js +73 -0
- package/dist/net/broadcast.js.map +1 -0
- package/dist/net/index.d.ts +5 -0
- package/dist/net/index.js +6 -0
- package/dist/net/online.d.ts +24 -0
- package/dist/net/online.d.ts.map +1 -0
- package/dist/net/online.js +26 -0
- package/dist/net/online.js.map +1 -0
- package/dist/net/sharedHost.d.ts +64 -0
- package/dist/net/sharedHost.d.ts.map +1 -0
- package/dist/net/sharedHost.js +124 -0
- package/dist/net/sharedHost.js.map +1 -0
- package/dist/net/sse.d.ts +115 -0
- package/dist/net/sse.d.ts.map +1 -0
- package/dist/net/sse.js +138 -0
- package/dist/net/sse.js.map +1 -0
- package/dist/net/websocket.d.ts +172 -0
- package/dist/net/websocket.d.ts.map +1 -0
- package/dist/net/websocket.js +199 -0
- package/dist/net/websocket.js.map +1 -0
- package/dist/observers/bounds.d.ts +24 -0
- package/dist/observers/bounds.d.ts.map +1 -0
- package/dist/observers/bounds.js +51 -0
- package/dist/observers/bounds.js.map +1 -0
- package/dist/observers/index.d.ts +4 -0
- package/dist/observers/index.js +5 -0
- package/dist/observers/intersection.d.ts +18 -0
- package/dist/observers/intersection.d.ts.map +1 -0
- package/dist/observers/intersection.js +33 -0
- package/dist/observers/intersection.js.map +1 -0
- package/dist/observers/mutation.d.ts +25 -0
- package/dist/observers/mutation.d.ts.map +1 -0
- package/dist/observers/mutation.js +53 -0
- package/dist/observers/mutation.js.map +1 -0
- package/dist/observers/resize.d.ts +18 -0
- package/dist/observers/resize.d.ts.map +1 -0
- package/dist/observers/resize.js +33 -0
- package/dist/observers/resize.js.map +1 -0
- package/dist/state/history.d.ts +115 -0
- package/dist/state/history.d.ts.map +1 -0
- package/dist/state/history.js +167 -0
- package/dist/state/history.js.map +1 -0
- package/dist/state/index.d.ts +6 -0
- package/dist/state/index.js +7 -0
- package/dist/state/locale.d.ts +27 -0
- package/dist/state/locale.d.ts.map +1 -0
- package/dist/state/locale.js +25 -0
- package/dist/state/locale.js.map +1 -0
- package/dist/state/pagination.d.ts +53 -0
- package/dist/state/pagination.d.ts.map +1 -0
- package/dist/state/pagination.js +72 -0
- package/dist/state/pagination.js.map +1 -0
- package/dist/state/range.d.ts +9 -0
- package/dist/state/range.d.ts.map +1 -0
- package/dist/state/range.js +18 -0
- package/dist/state/range.js.map +1 -0
- package/dist/state/selection.d.ts +47 -0
- package/dist/state/selection.d.ts.map +1 -0
- package/dist/state/selection.js +39 -0
- package/dist/state/selection.js.map +1 -0
- package/dist/state/virtual.d.ts +201 -0
- package/dist/state/virtual.d.ts.map +1 -0
- package/dist/state/{index.mjs → virtual.js} +11 -313
- package/dist/state/virtual.js.map +1 -0
- package/dist/time/debounce.d.ts +43 -0
- package/dist/time/debounce.d.ts.map +1 -0
- package/dist/time/debounce.js +65 -0
- package/dist/time/debounce.js.map +1 -0
- package/dist/time/index.d.ts +6 -0
- package/dist/time/index.js +7 -0
- package/dist/time/interval.d.ts +37 -0
- package/dist/time/interval.d.ts.map +1 -0
- package/dist/time/interval.js +52 -0
- package/dist/time/interval.js.map +1 -0
- package/dist/time/relative.d.ts +8 -0
- package/dist/time/relative.d.ts.map +1 -0
- package/dist/time/relative.js +27 -0
- package/dist/time/relative.js.map +1 -0
- package/dist/time/throttle.d.ts +38 -0
- package/dist/time/throttle.d.ts.map +1 -0
- package/dist/time/throttle.js +39 -0
- package/dist/time/throttle.js.map +1 -0
- package/dist/time/ticks.d.ts +21 -0
- package/dist/time/ticks.d.ts.map +1 -0
- package/dist/time/ticks.js +30 -0
- package/dist/time/ticks.js.map +1 -0
- package/dist/time/timer.d.ts +32 -0
- package/dist/time/timer.d.ts.map +1 -0
- package/dist/time/timer.js +50 -0
- package/dist/time/timer.js.map +1 -0
- package/package.json +31 -31
- package/dist/device/index.d.mts +0 -345
- package/dist/device/index.d.mts.map +0 -1
- package/dist/device/index.mjs +0 -498
- package/dist/device/index.mjs.map +0 -1
- package/dist/dom/index.d.mts +0 -750
- package/dist/dom/index.d.mts.map +0 -1
- package/dist/dom/index.mjs +0 -1857
- package/dist/dom/index.mjs.map +0 -1
- package/dist/editable-text-1D31qthY.d.mts +0 -319
- package/dist/editable-text-1D31qthY.d.mts.map +0 -1
- package/dist/events/index.d.mts +0 -184
- package/dist/events/index.d.mts.map +0 -1
- package/dist/events/index.mjs +0 -315
- package/dist/events/index.mjs.map +0 -1
- package/dist/follow-tab-stop-Caog2qg1.mjs +0 -863
- package/dist/follow-tab-stop-Caog2qg1.mjs.map +0 -1
- package/dist/index-CmmxqSEi.d.mts +0 -149
- package/dist/index-CmmxqSEi.d.mts.map +0 -1
- package/dist/index.d.mts +0 -12
- package/dist/index.mjs +0 -12
- package/dist/input-modality-BXD383jg.mjs.map +0 -1
- package/dist/input-modality-BY-bSrEc.d.mts.map +0 -1
- package/dist/interaction/index.d.mts +0 -1194
- package/dist/interaction/index.d.mts.map +0 -1
- package/dist/interaction/index.mjs +0 -2012
- package/dist/interaction/index.mjs.map +0 -1
- package/dist/internal-DP-xLKbT.mjs.map +0 -1
- package/dist/media/index.d.mts +0 -109
- package/dist/media/index.d.mts.map +0 -1
- package/dist/media/index.mjs +0 -149
- package/dist/media/index.mjs.map +0 -1
- package/dist/motion/index.d.mts +0 -2
- package/dist/motion/index.mjs +0 -1
- package/dist/motion-DmFDppRn.mjs +0 -240
- package/dist/motion-DmFDppRn.mjs.map +0 -1
- package/dist/net/index.d.mts +0 -348
- package/dist/net/index.d.mts.map +0 -1
- package/dist/net/index.mjs +0 -535
- package/dist/net/index.mjs.map +0 -1
- package/dist/observers/index.d.mts +0 -84
- package/dist/observers/index.d.mts.map +0 -1
- package/dist/observers/index.mjs +0 -159
- package/dist/observers/index.mjs.map +0 -1
- package/dist/rolldown-runtime-D7D4PA-g.mjs +0 -13
- package/dist/state/index.d.mts +0 -444
- package/dist/state/index.d.mts.map +0 -1
- package/dist/state/index.mjs.map +0 -1
- package/dist/time/index.d.mts +0 -179
- package/dist/time/index.d.mts.map +0 -1
- package/dist/time/index.mjs +0 -242
- package/dist/time/index.mjs.map +0 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Effect, Option, Queue, Schema } from "effect";
|
|
2
|
+
import { Bundle } from "foldkit-bundle";
|
|
3
|
+
import * as ManagedResource from "foldkit/managedResource";
|
|
4
|
+
/** The slice of a WebSocket the bundle needs; satisfied by the platform socket and test doubles. Handler events are `any`: the DOM and `ws` type them differently, and only `data` is read. */
|
|
5
|
+
export interface SocketHandle {
|
|
6
|
+
readonly readyState: number;
|
|
7
|
+
send(data: string): void;
|
|
8
|
+
close(): void;
|
|
9
|
+
onopen: ((event: any) => void) | null;
|
|
10
|
+
onmessage: ((event: any) => void) | null;
|
|
11
|
+
onclose: ((event: any) => void) | null;
|
|
12
|
+
onerror: ((event: any) => void) | null;
|
|
13
|
+
}
|
|
14
|
+
export declare const WebSocketModel: Schema.Struct<{
|
|
15
|
+
readonly url: Schema.String;
|
|
16
|
+
readonly status: Schema.Literals<readonly ["closed", "connecting", "open"]>;
|
|
17
|
+
readonly lastError: Schema.OptionFromNullOr<Schema.String>;
|
|
18
|
+
/** Whether this connection attempt ever reached open; reset on Connecting. */
|
|
19
|
+
readonly opened: Schema.Boolean;
|
|
20
|
+
}>;
|
|
21
|
+
export type WebSocketModel = typeof WebSocketModel.Type;
|
|
22
|
+
export declare const WebSocketMessage: import("foldkit/message").MessageUnion<{
|
|
23
|
+
readonly Connecting: {};
|
|
24
|
+
readonly Opened: {};
|
|
25
|
+
/** Notifies without storing: project the payload into your own field to keep it. */
|
|
26
|
+
readonly Received: {
|
|
27
|
+
data: Schema.String;
|
|
28
|
+
};
|
|
29
|
+
readonly Closed: {};
|
|
30
|
+
readonly Failed: {
|
|
31
|
+
message: Schema.String;
|
|
32
|
+
};
|
|
33
|
+
readonly SendFailed: {
|
|
34
|
+
message: Schema.String;
|
|
35
|
+
};
|
|
36
|
+
/** Acknowledges dispatch of `data`, not its delivery; update ignores it. */
|
|
37
|
+
readonly Sent: {
|
|
38
|
+
data: Schema.String;
|
|
39
|
+
};
|
|
40
|
+
/** The socket was still connecting when `connectTimeoutMs` ran out, and was closed. */
|
|
41
|
+
readonly TimedOut: {};
|
|
42
|
+
}>;
|
|
43
|
+
export type WebSocketMessage = typeof WebSocketMessage.Type;
|
|
44
|
+
/** The registry value behind the socket tag: the live socket and its incoming queue. */
|
|
45
|
+
export interface Acquired {
|
|
46
|
+
readonly socket: SocketHandle;
|
|
47
|
+
readonly events: Queue.Queue<WebSocketMessage>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* What the reader sees: the socket's state, or its error. A page derives
|
|
51
|
+
* this instead of keeping its own connection state machine. `lastError` is
|
|
52
|
+
* already worded for the reader (`Failed to connect to WebSocket`, never the
|
|
53
|
+
* socket's own text, which error handling drops in favor of the sentence).
|
|
54
|
+
*/
|
|
55
|
+
export declare const SocketView: import("foldkit/schema").TaggedUnion<{
|
|
56
|
+
readonly Disconnected: {};
|
|
57
|
+
readonly Connecting: {};
|
|
58
|
+
readonly Connected: {};
|
|
59
|
+
readonly Error: {
|
|
60
|
+
error: Schema.String;
|
|
61
|
+
};
|
|
62
|
+
}>;
|
|
63
|
+
export type SocketView = typeof SocketView.Type;
|
|
64
|
+
/**
|
|
65
|
+
* Whether the socket is usable: open with no error outstanding. Gates sends
|
|
66
|
+
* and received frames.
|
|
67
|
+
*/
|
|
68
|
+
export declare const isOpen: (model: WebSocketModel) => boolean;
|
|
69
|
+
/**
|
|
70
|
+
* The view of a socket, and whether its page still wants it. An error stands
|
|
71
|
+
* until the page stops wanting the socket; a retry still waiting for its
|
|
72
|
+
* socket reads as connecting, so it answers at once. An error on an open
|
|
73
|
+
* socket always reads as an error.
|
|
74
|
+
*/
|
|
75
|
+
export declare const viewOf: (model: WebSocketModel, wanted: boolean) => SocketView;
|
|
76
|
+
/** The live connection, for `send` and the incoming stream. One assembly holds one. */
|
|
77
|
+
export declare const Socket: ManagedResource.ManagedResource<Acquired, ManagedResource.ManagedResourceService<"websocket">>;
|
|
78
|
+
/** The socket service a placed WebSocket's streams and send commands require. */
|
|
79
|
+
export type SocketService = ManagedResource.ServiceOf<typeof Socket>;
|
|
80
|
+
/**
|
|
81
|
+
* A WebSocket as a bundle: connection lifecycle in the Model, full duplex
|
|
82
|
+
* through one Managed Resource. The resource owns the socket; a `send`
|
|
83
|
+
* helper writes through its tag service (lifted to the parent like every
|
|
84
|
+
* helper); a subscription stream reads incoming messages through the same
|
|
85
|
+
* tag. No hidden hub: everything the bundle touches is the Model, the
|
|
86
|
+
* resource registry, or a Message.
|
|
87
|
+
*/
|
|
88
|
+
export declare const websocket: <const Name extends string>(config: {
|
|
89
|
+
readonly name: Name;
|
|
90
|
+
/** Defaults to the platform WebSocket, read lazily so tests can substitute a double. */
|
|
91
|
+
readonly createSocket?: ((url: string) => SocketHandle) | undefined;
|
|
92
|
+
}) => Bundle.Bundle<Name, {
|
|
93
|
+
readonly url: string;
|
|
94
|
+
readonly connectTimeoutMs?: number | undefined;
|
|
95
|
+
}, {
|
|
96
|
+
readonly url: string;
|
|
97
|
+
readonly status: "closed" | "connecting" | "open";
|
|
98
|
+
readonly lastError: Option.Option<string>;
|
|
99
|
+
readonly opened: boolean;
|
|
100
|
+
}, {
|
|
101
|
+
readonly _tag: "Connecting";
|
|
102
|
+
} | {
|
|
103
|
+
readonly _tag: "Closed";
|
|
104
|
+
} | {
|
|
105
|
+
readonly _tag: "Failed";
|
|
106
|
+
readonly message: string;
|
|
107
|
+
} | {
|
|
108
|
+
readonly _tag: "Opened";
|
|
109
|
+
} | {
|
|
110
|
+
readonly _tag: "Received";
|
|
111
|
+
readonly data: string;
|
|
112
|
+
} | {
|
|
113
|
+
readonly _tag: "SendFailed";
|
|
114
|
+
readonly message: string;
|
|
115
|
+
} | {
|
|
116
|
+
readonly _tag: "Sent";
|
|
117
|
+
readonly data: string;
|
|
118
|
+
} | {
|
|
119
|
+
readonly _tag: "TimedOut";
|
|
120
|
+
}, never, ManagedResource.ManagedResourceService<"websocket">, ManagedResource.ManagedResourceService<"websocket">, void, {
|
|
121
|
+
socket: ManagedResource.Entry<{
|
|
122
|
+
readonly url: string;
|
|
123
|
+
readonly status: "closed" | "connecting" | "open";
|
|
124
|
+
readonly lastError: Option.Option<string>;
|
|
125
|
+
readonly opened: boolean;
|
|
126
|
+
}, {
|
|
127
|
+
readonly _tag: "Connecting";
|
|
128
|
+
} | {
|
|
129
|
+
readonly _tag: "Closed";
|
|
130
|
+
} | {
|
|
131
|
+
readonly _tag: "Failed";
|
|
132
|
+
readonly message: string;
|
|
133
|
+
} | {
|
|
134
|
+
readonly _tag: "Opened";
|
|
135
|
+
} | {
|
|
136
|
+
readonly _tag: "Received";
|
|
137
|
+
readonly data: string;
|
|
138
|
+
} | {
|
|
139
|
+
readonly _tag: "SendFailed";
|
|
140
|
+
readonly message: string;
|
|
141
|
+
} | {
|
|
142
|
+
readonly _tag: "Sent";
|
|
143
|
+
readonly data: string;
|
|
144
|
+
} | {
|
|
145
|
+
readonly _tag: "TimedOut";
|
|
146
|
+
}, Option.Option<string>, Acquired, ManagedResource.ManagedResourceService<"websocket">, () => {
|
|
147
|
+
readonly _tag: "Connecting";
|
|
148
|
+
}>;
|
|
149
|
+
}, {
|
|
150
|
+
send: (model: WebSocketModel, data: string) => {
|
|
151
|
+
model: {
|
|
152
|
+
readonly url: string;
|
|
153
|
+
readonly status: "closed" | "connecting" | "open";
|
|
154
|
+
readonly lastError: Option.Option<string>;
|
|
155
|
+
readonly opened: boolean;
|
|
156
|
+
};
|
|
157
|
+
commands: {
|
|
158
|
+
name: string;
|
|
159
|
+
args: {
|
|
160
|
+
data: string;
|
|
161
|
+
};
|
|
162
|
+
effect: Effect.Effect<{
|
|
163
|
+
readonly _tag: "Sent";
|
|
164
|
+
readonly data: string;
|
|
165
|
+
} | {
|
|
166
|
+
readonly _tag: "SendFailed";
|
|
167
|
+
readonly message: string;
|
|
168
|
+
}, never, ManagedResource.ManagedResourceService<"websocket">>;
|
|
169
|
+
}[];
|
|
170
|
+
};
|
|
171
|
+
}>;
|
|
172
|
+
//# sourceMappingURL=websocket.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.d.ts","names":[],"sources":["../../src/net/websocket.ts"],"mappings":";;;;iBAUiB;WACN;EACT,KAAK;EACL;EACA,UAAU;EACV,aAAa;EACb,WAAW;EACX,WAAW;;qBAGA,gBAAc,OAAA;;;;;;;YAOf,wBAAwB,eAAe;qBAEtC,4CAAgB;;;;;IAIf,MAAI,OAAA;;;;IAEN,SAAO,OAAA;;;IACH,SAAO,OAAA;;;;IAEb,MAAI,OAAA;;;;;YAIF,0BAA0B,iBAAiB;;iBAGtC;WACN,QAAQ;WACR,QAAQ,MAAM,MAAM;;;;;;;;qBASlB,qCAAU;;;;;IAIZ,OAAK,OAAA;;;YAEJ,oBAAoB,WAAW;;;;;qBAM9B,SAAM,OAAW;;;;;;;qBASjB,SAAM,OAAW,gBAAc,oBAAoB;;qBAYnD,QAAM,gBAAA,gBAAA,UAAA,gBAAA;;YAGP,gBAAgB,gBAAgB,iBAAiB;;;;;;;;;qBA+BhD,kBAAmB,qBAAmB;WACxC,MAAM;;WAEN,iBAAiB,gBAAgB;MAC3C,OAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2KmB,OAAA,OAAA,gBAAc"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { Effect, Option, Queue, Schema, Stream } from "effect";
|
|
2
|
+
import { defineMessageUnion } from "foldkit/message";
|
|
3
|
+
import * as Subscription from "foldkit/subscription";
|
|
4
|
+
import { Bundle } from "foldkit-bundle";
|
|
5
|
+
import { unlessSame } from "../internal.js";
|
|
6
|
+
import * as ManagedResource from "foldkit/managedResource";
|
|
7
|
+
import { defineTaggedUnion } from "foldkit/schema";
|
|
8
|
+
// src/net/websocket.ts
|
|
9
|
+
const WebSocketModel = Schema.Struct({
|
|
10
|
+
url: Schema.String,
|
|
11
|
+
status: Schema.Literals([
|
|
12
|
+
"closed",
|
|
13
|
+
"connecting",
|
|
14
|
+
"open"
|
|
15
|
+
]),
|
|
16
|
+
lastError: Schema.OptionFromNullOr(Schema.String),
|
|
17
|
+
/** Whether this connection attempt ever reached open; reset on Connecting. */
|
|
18
|
+
opened: Schema.Boolean
|
|
19
|
+
});
|
|
20
|
+
const WebSocketMessage = defineMessageUnion({
|
|
21
|
+
Connecting: {},
|
|
22
|
+
Opened: {},
|
|
23
|
+
/** Notifies without storing: project the payload into your own field to keep it. */
|
|
24
|
+
Received: { data: Schema.String },
|
|
25
|
+
Closed: {},
|
|
26
|
+
Failed: { message: Schema.String },
|
|
27
|
+
SendFailed: { message: Schema.String },
|
|
28
|
+
/** Acknowledges dispatch of `data`, not its delivery; update ignores it. */
|
|
29
|
+
Sent: { data: Schema.String },
|
|
30
|
+
/** The socket was still connecting when `connectTimeoutMs` ran out, and was closed. */
|
|
31
|
+
TimedOut: {}
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* What the reader sees: the socket's state, or its error. A page derives
|
|
35
|
+
* this instead of keeping its own connection state machine. `lastError` is
|
|
36
|
+
* already worded for the reader (`Failed to connect to WebSocket`, never the
|
|
37
|
+
* socket's own text, which error handling drops in favor of the sentence).
|
|
38
|
+
*/
|
|
39
|
+
const SocketView = defineTaggedUnion({
|
|
40
|
+
Disconnected: {},
|
|
41
|
+
Connecting: {},
|
|
42
|
+
Connected: {},
|
|
43
|
+
Error: { error: Schema.String }
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Whether the socket is usable: open with no error outstanding. Gates sends
|
|
47
|
+
* and received frames.
|
|
48
|
+
*/
|
|
49
|
+
const isOpen = (model) => model.status === "open" && Option.isNone(model.lastError);
|
|
50
|
+
/**
|
|
51
|
+
* The view of a socket, and whether its page still wants it. An error stands
|
|
52
|
+
* until the page stops wanting the socket; a retry still waiting for its
|
|
53
|
+
* socket reads as connecting, so it answers at once. An error on an open
|
|
54
|
+
* socket always reads as an error.
|
|
55
|
+
*/
|
|
56
|
+
const viewOf = (model, wanted) => Option.match(model.lastError, {
|
|
57
|
+
onSome: (error) => model.status === "open" || !wanted ? SocketView.Error({ error }) : SocketView.Connecting(),
|
|
58
|
+
onNone: () => {
|
|
59
|
+
if (model.status === "open") return SocketView.Connected();
|
|
60
|
+
if (model.status === "connecting" || wanted) return SocketView.Connecting();
|
|
61
|
+
return SocketView.Disconnected();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
/** The live connection, for `send` and the incoming stream. One assembly holds one. */
|
|
65
|
+
const Socket = ManagedResource.tag()("websocket");
|
|
66
|
+
const textOf = (data) => typeof data === "string" ? Effect.succeed(data) : data instanceof Blob ? Effect.promise(() => data.text()) : data instanceof ArrayBuffer || ArrayBuffer.isView(data) ? Effect.succeed(new TextDecoder().decode(data)) : Effect.succeed(String(data));
|
|
67
|
+
/** Closes the socket with its handlers detached first, so the close reports nothing. */
|
|
68
|
+
const closeSilently = (socket) => {
|
|
69
|
+
socket.onopen = null;
|
|
70
|
+
socket.onmessage = null;
|
|
71
|
+
socket.onclose = null;
|
|
72
|
+
socket.onerror = null;
|
|
73
|
+
socket.close();
|
|
74
|
+
};
|
|
75
|
+
const failMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
76
|
+
/**
|
|
77
|
+
* A WebSocket as a bundle: connection lifecycle in the Model, full duplex
|
|
78
|
+
* through one Managed Resource. The resource owns the socket; a `send`
|
|
79
|
+
* helper writes through its tag service (lifted to the parent like every
|
|
80
|
+
* helper); a subscription stream reads incoming messages through the same
|
|
81
|
+
* tag. No hidden hub: everything the bundle touches is the Model, the
|
|
82
|
+
* resource registry, or a Message.
|
|
83
|
+
*/
|
|
84
|
+
const websocket = (config) => {
|
|
85
|
+
const createSocket = config.createSocket ?? ((url) => new WebSocket(url));
|
|
86
|
+
return Bundle.make(config.name, {
|
|
87
|
+
Model: WebSocketModel,
|
|
88
|
+
Message: WebSocketMessage,
|
|
89
|
+
args: Schema.Struct({
|
|
90
|
+
url: Schema.String,
|
|
91
|
+
/** How long a socket may stay connecting before it is closed as `TimedOut`; unbounded when absent. */
|
|
92
|
+
connectTimeoutMs: Schema.optional(Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())))
|
|
93
|
+
}),
|
|
94
|
+
init: (args) => ({ model: {
|
|
95
|
+
url: args.url,
|
|
96
|
+
status: "closed",
|
|
97
|
+
lastError: Option.none(),
|
|
98
|
+
opened: false
|
|
99
|
+
} }),
|
|
100
|
+
update: (model, message) => WebSocketMessage.match(message, {
|
|
101
|
+
Connecting: () => ({ model: unlessSame(model, {
|
|
102
|
+
...model,
|
|
103
|
+
status: "connecting",
|
|
104
|
+
lastError: Option.none(),
|
|
105
|
+
opened: false
|
|
106
|
+
}) }),
|
|
107
|
+
Opened: () => ({ model: unlessSame(model, {
|
|
108
|
+
...model,
|
|
109
|
+
status: "open",
|
|
110
|
+
lastError: Option.none(),
|
|
111
|
+
opened: true
|
|
112
|
+
}) }),
|
|
113
|
+
Received: () => ({ model }),
|
|
114
|
+
Closed: () => ({ model: unlessSame(model, {
|
|
115
|
+
...model,
|
|
116
|
+
status: "closed"
|
|
117
|
+
}) }),
|
|
118
|
+
Failed: () => ({ model: unlessSame(model, {
|
|
119
|
+
...model,
|
|
120
|
+
status: "closed",
|
|
121
|
+
lastError: Option.some(model.status === "open" ? "Connection error" : "Failed to connect to WebSocket")
|
|
122
|
+
}) }),
|
|
123
|
+
SendFailed: () => ({ model: unlessSame(model, {
|
|
124
|
+
...model,
|
|
125
|
+
lastError: Option.some("Socket unavailable")
|
|
126
|
+
}) }),
|
|
127
|
+
Sent: () => ({ model }),
|
|
128
|
+
TimedOut: () => ({ model: unlessSame(model, {
|
|
129
|
+
...model,
|
|
130
|
+
status: "closed",
|
|
131
|
+
lastError: Option.some("Connection timeout")
|
|
132
|
+
}) })
|
|
133
|
+
}),
|
|
134
|
+
resources: () => ManagedResource.make()((entry) => ({ socket: entry(Schema.Option(Schema.String), {
|
|
135
|
+
resource: Socket,
|
|
136
|
+
modelToMaybeRequirements: (model) => Option.some(model.url),
|
|
137
|
+
acquire: (url) => Effect.gen(function* () {
|
|
138
|
+
const events = yield* Queue.unbounded();
|
|
139
|
+
const socket = createSocket(url);
|
|
140
|
+
let tail = Promise.resolve();
|
|
141
|
+
const enqueue = (effect) => {
|
|
142
|
+
const run = () => Effect.runPromise(effect).then(() => void 0, () => void 0);
|
|
143
|
+
tail = tail.then(run, run);
|
|
144
|
+
};
|
|
145
|
+
const offer = (message) => enqueue(Queue.offer(events, message));
|
|
146
|
+
socket.onopen = () => offer(WebSocketMessage.Opened());
|
|
147
|
+
socket.onmessage = (event) => {
|
|
148
|
+
enqueue(Effect.flatMap(textOf(event.data), (text) => Queue.offer(events, WebSocketMessage.Received({ data: text }))));
|
|
149
|
+
};
|
|
150
|
+
socket.onclose = () => offer(WebSocketMessage.Closed());
|
|
151
|
+
socket.onerror = () => offer(WebSocketMessage.Failed({ message: `websocket error for ${url}` }));
|
|
152
|
+
return {
|
|
153
|
+
socket,
|
|
154
|
+
events
|
|
155
|
+
};
|
|
156
|
+
}),
|
|
157
|
+
onAcquired: () => WebSocketMessage.Connecting(),
|
|
158
|
+
onReleased: () => WebSocketMessage.Closed(),
|
|
159
|
+
onAcquireError: (error) => WebSocketMessage.Failed({ message: failMessage(error) }),
|
|
160
|
+
release: ({ socket, events }) => Effect.asVoid(Effect.andThen(Effect.sync(() => closeSilently(socket)), Queue.shutdown(events)))
|
|
161
|
+
}) })),
|
|
162
|
+
subscriptions: ({ connectTimeoutMs }) => Subscription.make()((entry) => ({
|
|
163
|
+
connectTimeout: entry({ connecting: Schema.Boolean }, {
|
|
164
|
+
modelToDependencies: (model) => ({ connecting: model.status === "connecting" }),
|
|
165
|
+
dependenciesToStream: ({ connecting }) => !connecting || connectTimeoutMs === void 0 ? Stream.empty : Stream.unwrap(Effect.matchEffect(Effect.andThen(Effect.sleep(connectTimeoutMs), Socket.get), {
|
|
166
|
+
onFailure: () => Effect.succeed(Stream.empty),
|
|
167
|
+
onSuccess: ({ socket }) => Effect.sync(() => {
|
|
168
|
+
if (socket.readyState !== 0) return Stream.empty;
|
|
169
|
+
closeSilently(socket);
|
|
170
|
+
return Stream.make(WebSocketMessage.TimedOut());
|
|
171
|
+
})
|
|
172
|
+
}))
|
|
173
|
+
}),
|
|
174
|
+
incoming: entry({ live: Schema.Boolean }, {
|
|
175
|
+
modelToDependencies: (model) => ({ live: model.status !== "closed" }),
|
|
176
|
+
dependenciesToStream: ({ live }) => !live ? Stream.empty : Stream.unwrap(Effect.matchEffect(Socket.get, {
|
|
177
|
+
onFailure: () => Effect.succeed(Stream.empty),
|
|
178
|
+
onSuccess: ({ socket, events }) => Effect.succeed(Stream.concat(socket.readyState === 1 ? Stream.make(WebSocketMessage.Opened()) : Stream.empty, Stream.fromQueue(events)))
|
|
179
|
+
}))
|
|
180
|
+
})
|
|
181
|
+
})),
|
|
182
|
+
helpers: { send: (model, data) => ({
|
|
183
|
+
model,
|
|
184
|
+
commands: [{
|
|
185
|
+
name: "WebSocket.send",
|
|
186
|
+
args: { data },
|
|
187
|
+
effect: Effect.matchEffect(Effect.flatMap(Socket.get, ({ socket }) => socket.readyState === 1 ? Effect.map(Effect.sync(() => {
|
|
188
|
+
socket.send(data);
|
|
189
|
+
}), () => WebSocketMessage.Sent({ data })) : Effect.fail(/* @__PURE__ */ new Error(`cannot send: socket is not open (readyState ${socket.readyState})`))), {
|
|
190
|
+
onFailure: (error) => Effect.succeed(WebSocketMessage.SendFailed({ message: failMessage(error) })),
|
|
191
|
+
onSuccess: (message) => Effect.succeed(message)
|
|
192
|
+
})
|
|
193
|
+
}]
|
|
194
|
+
}) }
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
export { Socket, SocketView, WebSocketMessage, WebSocketModel, isOpen, viewOf, websocket };
|
|
198
|
+
|
|
199
|
+
//# sourceMappingURL=websocket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.js","names":[],"sources":["../../src/net/websocket.ts"],"sourcesContent":["import { Effect, Option, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { defineTaggedUnion } from 'foldkit/schema'\nimport * as ManagedResource from 'foldkit/managedResource'\nimport * as Subscription from 'foldkit/subscription'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\nimport { unlessSame } from '../internal.js'\n\n/** The slice of a WebSocket the bundle needs; satisfied by the platform socket and test doubles. Handler events are `any`: the DOM and `ws` type them differently, and only `data` is read. */\nexport interface SocketHandle {\n readonly readyState: number\n send(data: string): void\n close(): void\n onopen: ((event: any) => void) | null\n onmessage: ((event: any) => void) | null\n onclose: ((event: any) => void) | null\n onerror: ((event: any) => void) | null\n}\n\nexport const WebSocketModel = Schema.Struct({\n url: Schema.String,\n status: Schema.Literals(['closed', 'connecting', 'open']),\n lastError: Schema.OptionFromNullOr(Schema.String),\n /** Whether this connection attempt ever reached open; reset on Connecting. */\n opened: Schema.Boolean,\n})\nexport type WebSocketModel = typeof WebSocketModel.Type\n\nexport const WebSocketMessage = defineMessageUnion({\n Connecting: {},\n Opened: {},\n /** Notifies without storing: project the payload into your own field to keep it. */\n Received: { data: Schema.String },\n Closed: {},\n Failed: { message: Schema.String },\n SendFailed: { message: Schema.String },\n /** Acknowledges dispatch of `data`, not its delivery; update ignores it. */\n Sent: { data: Schema.String },\n /** The socket was still connecting when `connectTimeoutMs` ran out, and was closed. */\n TimedOut: {},\n})\nexport type WebSocketMessage = typeof WebSocketMessage.Type\n\n/** The registry value behind the socket tag: the live socket and its incoming queue. */\nexport interface Acquired {\n readonly socket: SocketHandle\n readonly events: Queue.Queue<WebSocketMessage>\n}\n\n/**\n * What the reader sees: the socket's state, or its error. A page derives\n * this instead of keeping its own connection state machine. `lastError` is\n * already worded for the reader (`Failed to connect to WebSocket`, never the\n * socket's own text, which error handling drops in favor of the sentence).\n */\nexport const SocketView = defineTaggedUnion({\n Disconnected: {},\n Connecting: {},\n Connected: {},\n Error: { error: Schema.String },\n})\nexport type SocketView = typeof SocketView.Type\n\n/**\n * Whether the socket is usable: open with no error outstanding. Gates sends\n * and received frames.\n */\nexport const isOpen = (model: WebSocketModel): boolean =>\n model.status === 'open' && Option.isNone(model.lastError)\n\n/**\n * The view of a socket, and whether its page still wants it. An error stands\n * until the page stops wanting the socket; a retry still waiting for its\n * socket reads as connecting, so it answers at once. An error on an open\n * socket always reads as an error.\n */\nexport const viewOf = (model: WebSocketModel, wanted: boolean): SocketView =>\n Option.match(model.lastError, {\n onSome: error =>\n model.status === 'open' || !wanted ? SocketView.Error({ error }) : SocketView.Connecting(),\n onNone: () => {\n if (model.status === 'open') return SocketView.Connected()\n if (model.status === 'connecting' || wanted) return SocketView.Connecting()\n return SocketView.Disconnected()\n },\n })\n\n/** The live connection, for `send` and the incoming stream. One assembly holds one. */\nexport const Socket = ManagedResource.tag<Acquired>()('websocket')\n\n/** The socket service a placed WebSocket's streams and send commands require. */\nexport type SocketService = ManagedResource.ServiceOf<typeof Socket>\n\nconst textOf = (data: unknown): Effect.Effect<string> =>\n typeof data === 'string'\n ? Effect.succeed(data)\n : data instanceof Blob\n ? Effect.promise(() => data.text())\n : data instanceof ArrayBuffer || ArrayBuffer.isView(data)\n ? Effect.succeed(new TextDecoder().decode(data))\n : Effect.succeed(String(data))\n\n/** Closes the socket with its handlers detached first, so the close reports nothing. */\nconst closeSilently = (socket: SocketHandle): void => {\n socket.onopen = null\n socket.onmessage = null\n socket.onclose = null\n socket.onerror = null\n socket.close()\n}\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\n/**\n * A WebSocket as a bundle: connection lifecycle in the Model, full duplex\n * through one Managed Resource. The resource owns the socket; a `send`\n * helper writes through its tag service (lifted to the parent like every\n * helper); a subscription stream reads incoming messages through the same\n * tag. No hidden hub: everything the bundle touches is the Model, the\n * resource registry, or a Message.\n */\nexport const websocket = <const Name extends string>(config: {\n readonly name: Name\n /** Defaults to the platform WebSocket, read lazily so tests can substitute a double. */\n readonly createSocket?: ((url: string) => SocketHandle) | undefined\n}) => {\n const createSocket: (url: string) => SocketHandle =\n config.createSocket ?? (url => new WebSocket(url) as unknown as SocketHandle)\n return Bundle.make(config.name, {\n Model: WebSocketModel,\n Message: WebSocketMessage,\n args: Schema.Struct({\n url: Schema.String,\n /** How long a socket may stay connecting before it is closed as `TimedOut`; unbounded when absent. */\n connectTimeoutMs: Schema.optional(\n Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())),\n ),\n }),\n init: (args): Update.Return<WebSocketModel, WebSocketMessage, SocketService> => ({\n model: { url: args.url, status: 'closed', lastError: Option.none(), opened: false },\n }),\n update: (\n model,\n message,\n ): Update.ReturnWithOutMessage<WebSocketModel, WebSocketMessage, never, SocketService> =>\n WebSocketMessage.match(message, {\n Connecting: () => ({\n model: unlessSame(model, {\n ...model,\n status: 'connecting',\n lastError: Option.none(),\n opened: false,\n }),\n }),\n Opened: () => ({\n model: unlessSame(model, {\n ...model,\n status: 'open',\n lastError: Option.none(),\n opened: true,\n }),\n }),\n Received: () => ({ model }),\n // The close that follows an error, and the release after it, find it closed.\n Closed: () => ({ model: unlessSame(model, { ...model, status: 'closed' }) }),\n // Worded by whether the socket had opened: the socket's own text is\n // dropped in favor of the sentence, as a page would word it.\n Failed: () => ({\n model: unlessSame(model, {\n ...model,\n status: 'closed',\n lastError: Option.some(\n model.status === 'open' ? 'Connection error' : 'Failed to connect to WebSocket',\n ),\n }),\n }),\n SendFailed: () => ({\n model: unlessSame(model, { ...model, lastError: Option.some('Socket unavailable') }),\n }),\n Sent: () => ({ model }),\n TimedOut: () => ({\n model: unlessSame(model, {\n ...model,\n status: 'closed',\n lastError: Option.some('Connection timeout'),\n }),\n }),\n }),\n resources: () =>\n ManagedResource.make<WebSocketModel, WebSocketMessage>()(entry => ({\n socket: entry(Schema.Option(Schema.String), {\n resource: Socket,\n modelToMaybeRequirements: model => Option.some(model.url),\n acquire: (url: string) =>\n Effect.gen(function* () {\n const events = yield* Queue.unbounded<WebSocketMessage>()\n const socket = createSocket(url)\n // Offers run in arrival order: an async decode (Blob text) must\n // not overtake a later message. Rejections settle the chain\n // without breaking it; shutdown races are expected at teardown.\n let tail: Promise<void> = Promise.resolve()\n const enqueue = (effect: Effect.Effect<unknown>): void => {\n const run = () =>\n Effect.runPromise(effect).then(\n () => undefined,\n () => undefined,\n )\n tail = tail.then(run, run)\n }\n const offer = (message: WebSocketMessage) => enqueue(Queue.offer(events, message))\n socket.onopen = () => offer(WebSocketMessage.Opened())\n socket.onmessage = event => {\n enqueue(\n Effect.flatMap(textOf(event.data), text =>\n Queue.offer(events, WebSocketMessage.Received({ data: text })),\n ),\n )\n }\n socket.onclose = () => offer(WebSocketMessage.Closed())\n socket.onerror = () =>\n offer(WebSocketMessage.Failed({ message: `websocket error for ${url}` }))\n return { socket, events }\n }),\n onAcquired: () => WebSocketMessage.Connecting(),\n onReleased: () => WebSocketMessage.Closed(),\n onAcquireError: error => WebSocketMessage.Failed({ message: failMessage(error) }),\n // Daemon teardown: close the socket and drain the queue.\n release: ({ socket, events }) =>\n Effect.asVoid(\n Effect.andThen(\n Effect.sync(() => closeSilently(socket)),\n Queue.shutdown(events),\n ),\n ),\n }),\n })),\n subscriptions: ({\n connectTimeoutMs,\n }): Subscription.Subscriptions<WebSocketModel, WebSocketMessage, SocketService> =>\n Subscription.make<WebSocketModel, WebSocketMessage, SocketService>()(entry => ({\n connectTimeout: entry(\n { connecting: Schema.Boolean },\n {\n modelToDependencies: model => ({ connecting: model.status === 'connecting' }),\n dependenciesToStream: ({ connecting }) =>\n !connecting || connectTimeoutMs === undefined\n ? Stream.empty\n : Stream.unwrap(\n Effect.matchEffect(Effect.andThen(Effect.sleep(connectTimeoutMs), Socket.get), {\n // Released while waiting: onReleased reports Closed.\n onFailure: () => Effect.succeed(Stream.empty),\n onSuccess: ({ socket }) =>\n Effect.sync((): Stream.Stream<WebSocketMessage> => {\n // 0 is WebSocket.CONNECTING. An open that beat the\n // timer wins; the handlers go before the close, so\n // the error and close it causes report nothing.\n if (socket.readyState !== 0) return Stream.empty\n closeSilently(socket)\n return Stream.make(WebSocketMessage.TimedOut())\n }),\n }),\n ),\n },\n ),\n incoming: entry(\n // Connecting to open is no reason to restart: the stream reads the\n // same queue, and replays an open it attached too late to see.\n { live: Schema.Boolean },\n {\n modelToDependencies: model => ({ live: model.status !== 'closed' }),\n dependenciesToStream: ({ live }) =>\n !live\n ? Stream.empty\n : Stream.unwrap(\n Effect.matchEffect(Socket.get, {\n // Released between the dependency read and attach: end\n // quietly; onReleased reports Closed through update.\n onFailure: () => Effect.succeed(Stream.empty),\n onSuccess: ({ socket, events }) =>\n Effect.succeed(\n Stream.concat(\n // A fast server opens before the stream attaches; without\n // this the status would stick at connecting while\n // messages flow. 1 is WebSocket.OPEN everywhere.\n socket.readyState === 1\n ? Stream.make(WebSocketMessage.Opened())\n : Stream.empty,\n Stream.fromQueue(events),\n ),\n ),\n }),\n ),\n },\n ),\n })),\n helpers: {\n send: (model: WebSocketModel, data: string) => ({\n model,\n commands: [\n {\n name: 'WebSocket.send',\n args: { data },\n effect: Effect.matchEffect(\n Effect.flatMap(Socket.get, ({ socket }) =>\n // A closed socket's send is a silent no-op per spec (only a\n // connecting socket throws), so check first: SendFailed either way.\n socket.readyState === 1\n ? Effect.map(\n Effect.sync(() => {\n socket.send(data)\n }),\n () => WebSocketMessage.Sent({ data }),\n )\n : Effect.fail(\n new Error(\n `cannot send: socket is not open (readyState ${socket.readyState})`,\n ),\n ),\n ),\n {\n onFailure: error =>\n Effect.succeed(WebSocketMessage.SendFailed({ message: failMessage(error) })),\n onSuccess: message => Effect.succeed(message),\n },\n ),\n },\n ],\n }),\n },\n })\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
export declare const Measured: Schema.TaggedStruct<"Measured", {
|
|
4
|
+
readonly x: Schema.Number;
|
|
5
|
+
readonly y: Schema.Number;
|
|
6
|
+
readonly width: Schema.Number;
|
|
7
|
+
readonly height: Schema.Number;
|
|
8
|
+
}>;
|
|
9
|
+
export type Measured = typeof Measured.Type;
|
|
10
|
+
/**
|
|
11
|
+
* Element bounds as a Mount: the bounding rect re-measured on
|
|
12
|
+
* ResizeObserver, window scroll, and window resize, starting with the
|
|
13
|
+
* current rect. Without a ResizeObserver the window events still measure —
|
|
14
|
+
* position changes never needed the observer. No window (SSR) emits nothing
|
|
15
|
+
* instead of throwing.
|
|
16
|
+
*/
|
|
17
|
+
export declare const Bounds: Mount.MountDefinitionNoArgs<"Bounds", {
|
|
18
|
+
readonly _tag: "Measured";
|
|
19
|
+
readonly x: number;
|
|
20
|
+
readonly y: number;
|
|
21
|
+
readonly width: number;
|
|
22
|
+
readonly height: number;
|
|
23
|
+
}>;
|
|
24
|
+
//# sourceMappingURL=bounds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bounds.d.ts","names":[],"sources":["../../src/observers/bounds.ts"],"mappings":";;qBAGa,UAAQ,OAAA;;;;;;YAMT,kBAAkB,SAAS;;;;;;;;qBAc1B,QAAM,MAAA"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Effect, Queue, Schema, Stream } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
// src/observers/bounds.ts
|
|
4
|
+
const Measured = Schema.TaggedStruct("Measured", {
|
|
5
|
+
x: Schema.Number,
|
|
6
|
+
y: Schema.Number,
|
|
7
|
+
width: Schema.Number,
|
|
8
|
+
height: Schema.Number
|
|
9
|
+
});
|
|
10
|
+
/**
|
|
11
|
+
* Element bounds as a Mount: the bounding rect re-measured on
|
|
12
|
+
* ResizeObserver, window scroll, and window resize, starting with the
|
|
13
|
+
* current rect. Without a ResizeObserver the window events still measure —
|
|
14
|
+
* position changes never needed the observer. No window (SSR) emits nothing
|
|
15
|
+
* instead of throwing.
|
|
16
|
+
*/
|
|
17
|
+
const Bounds = Mount.defineStream("Bounds", {
|
|
18
|
+
messages: [Measured],
|
|
19
|
+
execute: ({ element }) => Stream.callback((queue) => Effect.gen(function* () {
|
|
20
|
+
if (typeof window === "undefined") return;
|
|
21
|
+
let last = null;
|
|
22
|
+
const measure = () => {
|
|
23
|
+
const rect = element.getBoundingClientRect();
|
|
24
|
+
if (last !== null && last.x === rect.x && last.y === rect.y && last.width === rect.width && last.height === rect.height) return;
|
|
25
|
+
last = rect;
|
|
26
|
+
Queue.offerUnsafe(queue, Measured.make({
|
|
27
|
+
x: rect.x,
|
|
28
|
+
y: rect.y,
|
|
29
|
+
width: rect.width,
|
|
30
|
+
height: rect.height
|
|
31
|
+
}));
|
|
32
|
+
};
|
|
33
|
+
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
34
|
+
window.addEventListener("scroll", measure, true);
|
|
35
|
+
window.addEventListener("resize", measure);
|
|
36
|
+
const Observed = globalThis.ResizeObserver;
|
|
37
|
+
if (Observed === void 0) return null;
|
|
38
|
+
const observer = new Observed(measure);
|
|
39
|
+
observer.observe(element);
|
|
40
|
+
return observer;
|
|
41
|
+
}), (observer) => Effect.sync(() => {
|
|
42
|
+
window.removeEventListener("scroll", measure, true);
|
|
43
|
+
window.removeEventListener("resize", measure);
|
|
44
|
+
observer?.disconnect();
|
|
45
|
+
}));
|
|
46
|
+
measure();
|
|
47
|
+
}))
|
|
48
|
+
});
|
|
49
|
+
export { Bounds, Measured };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=bounds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bounds.js","names":[],"sources":["../../src/observers/bounds.ts"],"sourcesContent":["import { Effect, Queue, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nexport const Measured = Schema.TaggedStruct('Measured', {\n x: Schema.Number,\n y: Schema.Number,\n width: Schema.Number,\n height: Schema.Number,\n})\nexport type Measured = typeof Measured.Type\n\ntype ObserverCtor = new (callback: () => void) => {\n observe(element: Element): void\n disconnect(): void\n}\n\n/**\n * Element bounds as a Mount: the bounding rect re-measured on\n * ResizeObserver, window scroll, and window resize, starting with the\n * current rect. Without a ResizeObserver the window events still measure —\n * position changes never needed the observer. No window (SSR) emits nothing\n * instead of throwing.\n */\nexport const Bounds = Mount.defineStream('Bounds', {\n messages: [Measured],\n execute: ({ element }) =>\n Stream.callback<typeof Measured.Type>(queue =>\n Effect.gen(function* () {\n if (typeof window === 'undefined') return\n // Every scroll anywhere measures, and mounting measures twice (the\n // observer's first report and the call below): report only a change.\n let last: DOMRectReadOnly | null = null\n const measure = () => {\n const rect = element.getBoundingClientRect()\n if (\n last !== null &&\n last.x === rect.x &&\n last.y === rect.y &&\n last.width === rect.width &&\n last.height === rect.height\n ) {\n return\n }\n last = rect\n Queue.offerUnsafe(\n queue,\n Measured.make({ x: rect.x, y: rect.y, width: rect.width, height: rect.height }),\n )\n }\n // Capture phase: container scrolls do not bubble, but every scroll\n // descends past the window first.\n yield* Effect.acquireRelease(\n Effect.sync(() => {\n window.addEventListener('scroll', measure, true)\n window.addEventListener('resize', measure)\n const Observed = (globalThis as { ResizeObserver?: ObserverCtor }).ResizeObserver\n if (Observed === undefined) return null\n const observer = new Observed(measure)\n observer.observe(element)\n return observer\n }),\n observer =>\n Effect.sync(() => {\n window.removeEventListener('scroll', measure, true)\n window.removeEventListener('resize', measure)\n observer?.disconnect()\n }),\n )\n measure()\n }),\n ),\n})\n"],"mappings":";;;AAGA,MAAa,WAAW,OAAO,aAAa,YAAY;CACtD,GAAG,OAAO;CACV,GAAG,OAAO;CACV,OAAO,OAAO;CACd,QAAQ,OAAO;AACjB,CAAC;;;;;;;;AAeD,MAAa,SAAS,MAAM,aAAa,UAAU;CACjD,UAAU,CAAC,QAAQ;CACnB,UAAU,EAAE,cACV,OAAO,UAA+B,UACpC,OAAO,IAAI,aAAa;EACtB,IAAI,OAAO,WAAW,aAAa;EAGnC,IAAI,OAA+B;EACnC,MAAM,gBAAgB;GACpB,MAAM,OAAO,QAAQ,sBAAsB;GAC3C,IACE,SAAS,QACT,KAAK,MAAM,KAAK,KAChB,KAAK,MAAM,KAAK,KAChB,KAAK,UAAU,KAAK,SACpB,KAAK,WAAW,KAAK,QAErB;GAEF,OAAO;GACP,MAAM,YACJ,OACA,SAAS,KAAK;IAAE,GAAG,KAAK;IAAG,GAAG,KAAK;IAAG,OAAO,KAAK;IAAO,QAAQ,KAAK;GAAO,CAAC,CAChF;EACF;EAGA,OAAO,OAAO,eACZ,OAAO,WAAW;GAChB,OAAO,iBAAiB,UAAU,SAAS,IAAI;GAC/C,OAAO,iBAAiB,UAAU,OAAO;GACzC,MAAM,WAAY,WAAiD;GACnE,IAAI,aAAa,KAAA,GAAW,OAAO;GACnC,MAAM,WAAW,IAAI,SAAS,OAAO;GACrC,SAAS,QAAQ,OAAO;GACxB,OAAO;EACT,CAAC,IACD,aACE,OAAO,WAAW;GAChB,OAAO,oBAAoB,UAAU,SAAS,IAAI;GAClD,OAAO,oBAAoB,UAAU,OAAO;GAC5C,UAAU,WAAW;EACvB,CAAC,CACL;EACA,QAAQ;CACV,CAAC,CACH;AACJ,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
export declare const IntersectionChanged: Schema.TaggedStruct<"IntersectionChanged", {
|
|
4
|
+
readonly isIntersecting: Schema.Boolean;
|
|
5
|
+
readonly ratio: Schema.Number;
|
|
6
|
+
}>;
|
|
7
|
+
export type IntersectionChanged = typeof IntersectionChanged.Type;
|
|
8
|
+
/**
|
|
9
|
+
* Element visibility as a Mount: IntersectionObserver reports viewport
|
|
10
|
+
* crossings as Messages for the element carrying this Mount. No observer
|
|
11
|
+
* (SSR, old browser) emits nothing instead of throwing.
|
|
12
|
+
*/
|
|
13
|
+
export declare const Intersection: Mount.MountDefinitionNoArgs<"Intersection", {
|
|
14
|
+
readonly _tag: "IntersectionChanged";
|
|
15
|
+
readonly isIntersecting: boolean;
|
|
16
|
+
readonly ratio: number;
|
|
17
|
+
}>;
|
|
18
|
+
//# sourceMappingURL=intersection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intersection.d.ts","names":[],"sources":["../../src/observers/intersection.ts"],"mappings":";;qBAGa,qBAAmB,OAAA;;;;YAIpB,6BAA6B,oBAAoB;;;;;;qBAShD,cAAY,MAAA"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Effect, Queue, Schema, Stream } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
// src/observers/intersection.ts
|
|
4
|
+
const IntersectionChanged = Schema.TaggedStruct("IntersectionChanged", {
|
|
5
|
+
isIntersecting: Schema.Boolean,
|
|
6
|
+
ratio: Schema.Number
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* Element visibility as a Mount: IntersectionObserver reports viewport
|
|
10
|
+
* crossings as Messages for the element carrying this Mount. No observer
|
|
11
|
+
* (SSR, old browser) emits nothing instead of throwing.
|
|
12
|
+
*/
|
|
13
|
+
const Intersection = Mount.defineStream("Intersection", {
|
|
14
|
+
messages: [IntersectionChanged],
|
|
15
|
+
execute: ({ element }) => Stream.callback((queue) => Effect.gen(function* () {
|
|
16
|
+
const Observed = globalThis.IntersectionObserver;
|
|
17
|
+
if (Observed === void 0) return;
|
|
18
|
+
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
19
|
+
const observer = new Observed((entries) => {
|
|
20
|
+
const first = entries[0];
|
|
21
|
+
if (first !== void 0) Queue.offerUnsafe(queue, IntersectionChanged.make({
|
|
22
|
+
isIntersecting: first.isIntersecting,
|
|
23
|
+
ratio: first.intersectionRatio
|
|
24
|
+
}));
|
|
25
|
+
});
|
|
26
|
+
observer.observe(element);
|
|
27
|
+
return observer;
|
|
28
|
+
}), (observer) => Effect.sync(() => observer.disconnect()));
|
|
29
|
+
}))
|
|
30
|
+
});
|
|
31
|
+
export { Intersection, IntersectionChanged };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=intersection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intersection.js","names":[],"sources":["../../src/observers/intersection.ts"],"sourcesContent":["import { Effect, Queue, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nexport const IntersectionChanged = Schema.TaggedStruct('IntersectionChanged', {\n isIntersecting: Schema.Boolean,\n ratio: Schema.Number,\n})\nexport type IntersectionChanged = typeof IntersectionChanged.Type\n\ntype ObserverCtor = new (callback: IntersectionObserverCallback) => IntersectionObserver\n\n/**\n * Element visibility as a Mount: IntersectionObserver reports viewport\n * crossings as Messages for the element carrying this Mount. No observer\n * (SSR, old browser) emits nothing instead of throwing.\n */\nexport const Intersection = Mount.defineStream('Intersection', {\n messages: [IntersectionChanged],\n execute: ({ element }) =>\n Stream.callback<typeof IntersectionChanged.Type>(queue =>\n Effect.gen(function* () {\n const Observed = (globalThis as { IntersectionObserver?: ObserverCtor })\n .IntersectionObserver\n if (Observed === undefined) return\n yield* Effect.acquireRelease(\n Effect.sync(() => {\n const observer = new Observed(entries => {\n const first = entries[0]\n if (first !== undefined) {\n Queue.offerUnsafe(\n queue,\n IntersectionChanged.make({\n isIntersecting: first.isIntersecting,\n ratio: first.intersectionRatio,\n }),\n )\n }\n })\n observer.observe(element)\n return observer\n }),\n observer => Effect.sync(() => observer.disconnect()),\n )\n }),\n ),\n})\n"],"mappings":";;;AAGA,MAAa,sBAAsB,OAAO,aAAa,uBAAuB;CAC5E,gBAAgB,OAAO;CACvB,OAAO,OAAO;AAChB,CAAC;;;;;;AAUD,MAAa,eAAe,MAAM,aAAa,gBAAgB;CAC7D,UAAU,CAAC,mBAAmB;CAC9B,UAAU,EAAE,cACV,OAAO,UAA0C,UAC/C,OAAO,IAAI,aAAa;EACtB,MAAM,WAAY,WACf;EACH,IAAI,aAAa,KAAA,GAAW;EAC5B,OAAO,OAAO,eACZ,OAAO,WAAW;GAChB,MAAM,WAAW,IAAI,UAAS,YAAW;IACvC,MAAM,QAAQ,QAAQ;IACtB,IAAI,UAAU,KAAA,GACZ,MAAM,YACJ,OACA,oBAAoB,KAAK;KACvB,gBAAgB,MAAM;KACtB,OAAO,MAAM;IACf,CAAC,CACH;GAEJ,CAAC;GACD,SAAS,QAAQ,OAAO;GACxB,OAAO;EACT,CAAC,IACD,aAAY,OAAO,WAAW,SAAS,WAAW,CAAC,CACrD;CACF,CAAC,CACH;AACJ,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import * as Mount from "foldkit/mount";
|
|
3
|
+
export declare const Mutated: Schema.TaggedStruct<"Mutated", {
|
|
4
|
+
readonly type: Schema.Literals<readonly ["childList", "attributes", "characterData"]>;
|
|
5
|
+
readonly added: Schema.$Array<Schema.String>;
|
|
6
|
+
readonly removed: Schema.$Array<Schema.String>;
|
|
7
|
+
readonly attribute: Schema.NullOr<Schema.String>;
|
|
8
|
+
}>;
|
|
9
|
+
export type Mutated = typeof Mutated.Type;
|
|
10
|
+
/**
|
|
11
|
+
* DOM mutations as a Mount: MutationObserver reports child, attribute, and
|
|
12
|
+
* text changes as Messages for the element carrying this Mount. Nodes cross
|
|
13
|
+
* as their names (serializable); the live nodes stay in the DOM, never in
|
|
14
|
+
* the Model. No observer (SSR, old browser) emits nothing instead of
|
|
15
|
+
* throwing. Observes the whole subtree — narrow by placing on a smaller
|
|
16
|
+
* element, not by options.
|
|
17
|
+
*/
|
|
18
|
+
export declare const Mutation: Mount.MountDefinitionNoArgs<"Mutation", {
|
|
19
|
+
readonly _tag: "Mutated";
|
|
20
|
+
readonly type: "attributes" | "characterData" | "childList";
|
|
21
|
+
readonly added: readonly string[];
|
|
22
|
+
readonly removed: readonly string[];
|
|
23
|
+
readonly attribute: string | null;
|
|
24
|
+
}>;
|
|
25
|
+
//# sourceMappingURL=mutation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutation.d.ts","names":[],"sources":["../../src/observers/mutation.ts"],"mappings":";;qBAGa,SAAO,OAAA;;;;;;YAMR,iBAAiB,QAAQ;;;;;;;;;qBAexB,UAAQ,MAAA"}
|