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,24 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { Bundle } from "foldkit-bundle";
|
|
3
|
+
export declare const OnlineModel: Schema.Struct<{
|
|
4
|
+
readonly online: Schema.Boolean;
|
|
5
|
+
}>;
|
|
6
|
+
export type OnlineModel = typeof OnlineModel.Type;
|
|
7
|
+
export declare const OnlineMessage: import("foldkit/message").MessageUnion<{
|
|
8
|
+
readonly Changed: {
|
|
9
|
+
online: Schema.Boolean;
|
|
10
|
+
};
|
|
11
|
+
}>;
|
|
12
|
+
export type OnlineMessage = typeof OnlineMessage.Type;
|
|
13
|
+
/**
|
|
14
|
+
* Online presence as a bundle: `online` in the Model, kept current by the
|
|
15
|
+
* window's online/offline events. The browser reports facts; the Model owns
|
|
16
|
+
* the state, so presence replays like any other transition.
|
|
17
|
+
*/
|
|
18
|
+
export declare const Online: Bundle.Bundle<"Online", void, {
|
|
19
|
+
readonly online: boolean;
|
|
20
|
+
}, {
|
|
21
|
+
readonly _tag: "Changed";
|
|
22
|
+
readonly online: boolean;
|
|
23
|
+
}, never, never, never, void, {}, {}>;
|
|
24
|
+
//# sourceMappingURL=online.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"online.d.ts","names":[],"sources":["../../src/net/online.ts"],"mappings":";;qBAKa,aAAW,OAAA;;;YACZ,qBAAqB,YAAY;qBAEhC,yCAAa;;IAAmC,QAAM,OAAA;;;YACvD,uBAAuB,cAAc;;;;;;qBAuBpC,QAAM,OAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Schema, Stream } from "effect";
|
|
2
|
+
import { defineMessageUnion } from "foldkit/message";
|
|
3
|
+
import * as Subscription from "foldkit/subscription";
|
|
4
|
+
import { Bundle } from "foldkit-bundle";
|
|
5
|
+
// src/net/online.ts
|
|
6
|
+
const OnlineModel = Schema.Struct({ online: Schema.Boolean });
|
|
7
|
+
const OnlineMessage = defineMessageUnion({ Changed: { online: Schema.Boolean } });
|
|
8
|
+
const presenceStream = () => {
|
|
9
|
+
if (typeof window === "undefined") return Stream.empty;
|
|
10
|
+
return Stream.mergeAll([Stream.fromEventListener(window, "online").pipe(Stream.map(() => OnlineMessage.Changed({ online: true }))), Stream.fromEventListener(window, "offline").pipe(Stream.map(() => OnlineMessage.Changed({ online: false })))], { concurrency: "unbounded" });
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Online presence as a bundle: `online` in the Model, kept current by the
|
|
14
|
+
* window's online/offline events. The browser reports facts; the Model owns
|
|
15
|
+
* the state, so presence replays like any other transition.
|
|
16
|
+
*/
|
|
17
|
+
const Online = Bundle.make("Online", {
|
|
18
|
+
Model: OnlineModel,
|
|
19
|
+
Message: OnlineMessage,
|
|
20
|
+
init: () => ({ model: { online: typeof navigator !== "undefined" && typeof navigator.onLine === "boolean" ? navigator.onLine : true } }),
|
|
21
|
+
update: (model, message) => message.online === model.online ? { model } : { model: { online: message.online } },
|
|
22
|
+
subscriptions: () => Subscription.make()(() => ({ changes: Subscription.persistentEntry(presenceStream()) }))
|
|
23
|
+
});
|
|
24
|
+
export { Online, OnlineMessage, OnlineModel };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=online.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"online.js","names":[],"sources":["../../src/net/online.ts"],"sourcesContent":["import { Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const OnlineModel = Schema.Struct({ online: Schema.Boolean })\nexport type OnlineModel = typeof OnlineModel.Type\n\nexport const OnlineMessage = defineMessageUnion({ Changed: { online: Schema.Boolean } })\nexport type OnlineMessage = typeof OnlineMessage.Type\n\nconst presenceStream = (): Stream.Stream<OnlineMessage> => {\n // No window (SSR): nothing to listen to, so the slice keeps its default.\n if (typeof window === 'undefined') return Stream.empty\n return Stream.mergeAll(\n [\n Stream.fromEventListener(window, 'online').pipe(\n Stream.map(() => OnlineMessage.Changed({ online: true })),\n ),\n Stream.fromEventListener(window, 'offline').pipe(\n Stream.map(() => OnlineMessage.Changed({ online: false })),\n ),\n ],\n { concurrency: 'unbounded' },\n )\n}\n\n/**\n * Online presence as a bundle: `online` in the Model, kept current by the\n * window's online/offline events. The browser reports facts; the Model owns\n * the state, so presence replays like any other transition.\n */\nexport const Online = Bundle.make('Online', {\n Model: OnlineModel,\n Message: OnlineMessage,\n init: () => ({\n // navigator.onLine is the browser's current answer; true elsewhere (SSR).\n model: {\n online:\n typeof navigator !== 'undefined' && typeof navigator.onLine === 'boolean'\n ? navigator.onLine\n : true,\n },\n }),\n update: (model, message) =>\n message.online === model.online ? { model } : { model: { online: message.online } },\n subscriptions: (): Subscription.Subscriptions<OnlineModel, OnlineMessage> =>\n Subscription.make<OnlineModel, OnlineMessage>()(() => ({\n changes: Subscription.persistentEntry(presenceStream()),\n })),\n})\n"],"mappings":";;;;;AAKA,MAAa,cAAc,OAAO,OAAO,EAAE,QAAQ,OAAO,QAAQ,CAAC;AAGnE,MAAa,gBAAgB,mBAAmB,EAAE,SAAS,EAAE,QAAQ,OAAO,QAAQ,EAAE,CAAC;AAGvF,MAAM,uBAAqD;CAEzD,IAAI,OAAO,WAAW,aAAa,OAAO,OAAO;CACjD,OAAO,OAAO,SACZ,CACE,OAAO,kBAAkB,QAAQ,QAAQ,CAAC,CAAC,KACzC,OAAO,UAAU,cAAc,QAAQ,EAAE,QAAQ,KAAK,CAAC,CAAC,CAC1D,GACA,OAAO,kBAAkB,QAAQ,SAAS,CAAC,CAAC,KAC1C,OAAO,UAAU,cAAc,QAAQ,EAAE,QAAQ,MAAM,CAAC,CAAC,CAC3D,CACF,GACA,EAAE,aAAa,YAAY,CAC7B;AACF;;;;;;AAOA,MAAa,SAAS,OAAO,KAAK,UAAU;CAC1C,OAAO;CACP,SAAS;CACT,aAAa,EAEX,OAAO,EACL,QACE,OAAO,cAAc,eAAe,OAAO,UAAU,WAAW,YAC5D,UAAU,SACV,KACR,EACF;CACA,SAAS,OAAO,YACd,QAAQ,WAAW,MAAM,SAAS,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,QAAQ,QAAQ,OAAO,EAAE;CACpF,qBACE,aAAa,KAAiC,CAAC,QAAQ,EACrD,SAAS,aAAa,gBAAgB,eAAe,CAAC,EACxD,EAAE;AACN,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** One conversation a page opened: its port, and a signal that aborts once the page is gone. */
|
|
3
|
+
export interface Conversation {
|
|
4
|
+
readonly port: MessagePort;
|
|
5
|
+
readonly signal: AbortSignal;
|
|
6
|
+
}
|
|
7
|
+
/** What the host does with each conversation, told what the page opened it for. */
|
|
8
|
+
export type ConversationHandler<Opening> = (opening: Opening, conversation: Conversation) => void;
|
|
9
|
+
/** Starts the host, once, the first time a conversation needs it. */
|
|
10
|
+
export type StartHost<Opening> = () => ConversationHandler<Opening> | Promise<ConversationHandler<Opening>>;
|
|
11
|
+
/** Where `SharedHost` runs the host: in a SharedWorker, or in the page when there is none. */
|
|
12
|
+
export interface SharedHostOptions<Opening> {
|
|
13
|
+
/**
|
|
14
|
+
* The SharedWorker whose entry calls `serve`. Constructed by the
|
|
15
|
+
* application, since a bundler finds a worker only in a literal
|
|
16
|
+
* `new SharedWorker(new URL('./worker.ts', import.meta.url), …)`.
|
|
17
|
+
*/
|
|
18
|
+
readonly worker: () => {
|
|
19
|
+
readonly port: MessagePort;
|
|
20
|
+
};
|
|
21
|
+
/** The host, for a browser without SharedWorker: started in the top document. */
|
|
22
|
+
readonly inPage: StartHost<Opening>;
|
|
23
|
+
}
|
|
24
|
+
/** A page's way to the host. */
|
|
25
|
+
export interface SharedHostConnection<Opening> {
|
|
26
|
+
/** Opens a conversation for `opening`, returning the page's end of it. */
|
|
27
|
+
readonly open: (opening: Opening) => MessagePort;
|
|
28
|
+
}
|
|
29
|
+
export interface SharedHost<Opening> {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
/** A SharedWorker's entry: hands the host every conversation a page opens. */
|
|
32
|
+
readonly serve: (start: StartHost<Opening>) => void;
|
|
33
|
+
/**
|
|
34
|
+
* A page's connection: through the SharedWorker, or, without one, to the
|
|
35
|
+
* host in the top document. Call it in the top document too when that
|
|
36
|
+
* document frames the pages, so that it is the one the frames meet.
|
|
37
|
+
*/
|
|
38
|
+
readonly connect: (options: SharedHostOptions<Opening>) => SharedHostConnection<Opening>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A shared host: its `name`, which keeps two on one origin apart, and
|
|
42
|
+
* the schema of what opens a conversation, which the host decodes before
|
|
43
|
+
* anything else, ignoring a message that does not decode.
|
|
44
|
+
*/
|
|
45
|
+
declare const define: <Opening, Encoded>(options: {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly opening: Schema.Codec<Opening, Encoded, never, never>;
|
|
48
|
+
}) => SharedHost<Opening>;
|
|
49
|
+
/**
|
|
50
|
+
* One server in the browser for many documents: every tab and frame of an
|
|
51
|
+
* application meets the same host. A page opens conversations, each a
|
|
52
|
+
* `MessagePort` with a decoded opening that says what it is for; the host, in
|
|
53
|
+
* a SharedWorker shared by every tab, or in the top document where there is no
|
|
54
|
+
* SharedWorker, is handed each one with a signal that aborts when the page
|
|
55
|
+
* that opened it is gone.
|
|
56
|
+
*
|
|
57
|
+
* It owns the host's lifetime and the routing of conversations, nothing
|
|
58
|
+
* spoken over them: Sync's `portSocket` and Remote's `port` do that.
|
|
59
|
+
*/
|
|
60
|
+
export declare const SharedHost: {
|
|
61
|
+
define: typeof define;
|
|
62
|
+
};
|
|
63
|
+
export {};
|
|
64
|
+
//# sourceMappingURL=sharedHost.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sharedHost.d.ts","names":[],"sources":["../../src/net/sharedHost.ts"],"mappings":";;iBAGiB;WACN,MAAM;WACN,QAAQ;;;YAIP,oBAAoB,YAAY,SAAS,SAAS,cAAc;;YAGhE,UAAU,iBAClB,oBAAoB,WACpB,QAAQ,oBAAoB;;iBAGf,kBAAkB;;;;;;WAMxB;aAAyB,MAAM;;;WAE/B,QAAQ,UAAU;;;iBAIZ,qBAAqB;;WAE3B,OAAO,SAAS,YAAY;;iBAGtB,WAAW;WACjB;;WAEA,QAAQ,OAAO,UAAU;;;;;;WAMzB,UAAU,SAAS,kBAAkB,aAAa,qBAAqB;;;;;;;cAQ5E,SAAU,SAAS,SAAO;WACrB;WACA,SAAS,OAAO,MAAM,SAAS;MACtC,WAAW;;;;;;;;;;;;qBA4IF"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Option, Schema } from "effect";
|
|
2
|
+
// src/net/sharedHost.ts
|
|
3
|
+
/**
|
|
4
|
+
* A shared host: its `name`, which keeps two on one origin apart, and
|
|
5
|
+
* the schema of what opens a conversation, which the host decodes before
|
|
6
|
+
* anything else, ignoring a message that does not decode.
|
|
7
|
+
*/
|
|
8
|
+
const define = (options) => {
|
|
9
|
+
const { name } = options;
|
|
10
|
+
const Envelope = Schema.Struct({
|
|
11
|
+
host: Schema.Literal(name),
|
|
12
|
+
conversation: Schema.String,
|
|
13
|
+
opening: options.opening
|
|
14
|
+
});
|
|
15
|
+
const encode = Schema.encodeSync(Envelope);
|
|
16
|
+
const decode = Schema.decodeUnknownOption(Envelope);
|
|
17
|
+
const lockOf = (conversation) => `foldkit-shared-host:${name}:${conversation}`;
|
|
18
|
+
const marker = `foldkit-shared-host:${name}`;
|
|
19
|
+
const receive = (host, data, port) => Option.match(Option.zipWith(decode(data), Option.fromUndefinedOr(port), (envelope, given) => ({
|
|
20
|
+
envelope,
|
|
21
|
+
given
|
|
22
|
+
})), {
|
|
23
|
+
onNone: () => {},
|
|
24
|
+
onSome: ({ envelope, given }) => {
|
|
25
|
+
const ended = new AbortController();
|
|
26
|
+
Option.match(locks(), {
|
|
27
|
+
onNone: () => {},
|
|
28
|
+
onSome: (held) => {
|
|
29
|
+
held.request(lockOf(envelope.conversation), () => ended.abort());
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
host().then((started) => started(envelope.opening, {
|
|
33
|
+
port: given,
|
|
34
|
+
signal: ended.signal
|
|
35
|
+
})).catch(reportError);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
const once = (start) => {
|
|
39
|
+
let started;
|
|
40
|
+
return () => started ??= Promise.resolve().then(start);
|
|
41
|
+
};
|
|
42
|
+
const serve = (start) => {
|
|
43
|
+
const host = once(start);
|
|
44
|
+
self.addEventListener("connect", (event) => {
|
|
45
|
+
if (!(event instanceof MessageEvent)) return;
|
|
46
|
+
const [connection] = event.ports;
|
|
47
|
+
connection?.addEventListener("message", (message) => receive(host, message.data, message.ports[0]));
|
|
48
|
+
connection?.start();
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const connect = ({ worker, inPage }) => {
|
|
52
|
+
const send = routeOf(worker, inPage);
|
|
53
|
+
return { open: (opening) => {
|
|
54
|
+
const { port1, port2 } = new MessageChannel();
|
|
55
|
+
const conversation = crypto.randomUUID();
|
|
56
|
+
const envelope = encode({
|
|
57
|
+
host: name,
|
|
58
|
+
conversation,
|
|
59
|
+
opening
|
|
60
|
+
});
|
|
61
|
+
hold(lockOf(conversation)).then(() => send(envelope, port2));
|
|
62
|
+
return port1;
|
|
63
|
+
} };
|
|
64
|
+
};
|
|
65
|
+
const routeOf = (worker, inPage) => {
|
|
66
|
+
if (typeof SharedWorker !== "undefined") {
|
|
67
|
+
const { port } = worker();
|
|
68
|
+
return (envelope, given) => port.postMessage(envelope, [given]);
|
|
69
|
+
}
|
|
70
|
+
return Option.match(acceptingTop(marker), {
|
|
71
|
+
onSome: (top) => (envelope, given) => top.postMessage(envelope, location.origin, [given]),
|
|
72
|
+
onNone: () => {
|
|
73
|
+
const host = once(inPage);
|
|
74
|
+
Reflect.set(window, marker, true);
|
|
75
|
+
window.addEventListener("message", (event) => {
|
|
76
|
+
if (event.origin === location.origin) receive(host, event.data, event.ports[0]);
|
|
77
|
+
});
|
|
78
|
+
return (envelope, given) => receive(host, envelope, given);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
name,
|
|
84
|
+
serve,
|
|
85
|
+
connect
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
/** The top document, when it is another of this origin's and runs this host. */
|
|
89
|
+
const acceptingTop = (marker) => {
|
|
90
|
+
const top = window.top;
|
|
91
|
+
if (top === null || top === window) return Option.none();
|
|
92
|
+
try {
|
|
93
|
+
return Reflect.get(top, marker) === true ? Option.some(top) : Option.none();
|
|
94
|
+
} catch {
|
|
95
|
+
return Option.none();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
/** Resolves once this document holds `lock`, which it then holds while it lives. */
|
|
99
|
+
const hold = (lock) => Option.match(locks(), {
|
|
100
|
+
onNone: () => Promise.resolve(),
|
|
101
|
+
onSome: (held) => new Promise((granted) => {
|
|
102
|
+
held.request(lock, () => {
|
|
103
|
+
granted();
|
|
104
|
+
return new Promise(() => {});
|
|
105
|
+
});
|
|
106
|
+
})
|
|
107
|
+
});
|
|
108
|
+
/** Web Locks, which a page outside a secure context, or an older browser, lacks. */
|
|
109
|
+
const locks = () => Option.fromUndefinedOr(navigator.locks);
|
|
110
|
+
/**
|
|
111
|
+
* One server in the browser for many documents: every tab and frame of an
|
|
112
|
+
* application meets the same host. A page opens conversations, each a
|
|
113
|
+
* `MessagePort` with a decoded opening that says what it is for; the host, in
|
|
114
|
+
* a SharedWorker shared by every tab, or in the top document where there is no
|
|
115
|
+
* SharedWorker, is handed each one with a signal that aborts when the page
|
|
116
|
+
* that opened it is gone.
|
|
117
|
+
*
|
|
118
|
+
* It owns the host's lifetime and the routing of conversations, nothing
|
|
119
|
+
* spoken over them: Sync's `portSocket` and Remote's `port` do that.
|
|
120
|
+
*/
|
|
121
|
+
const SharedHost = { define };
|
|
122
|
+
export { SharedHost };
|
|
123
|
+
|
|
124
|
+
//# sourceMappingURL=sharedHost.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sharedHost.js","names":[],"sources":["../../src/net/sharedHost.ts"],"sourcesContent":["import { Option, Schema } from 'effect'\n\n/** One conversation a page opened: its port, and a signal that aborts once the page is gone. */\nexport interface Conversation {\n readonly port: MessagePort\n readonly signal: AbortSignal\n}\n\n/** What the host does with each conversation, told what the page opened it for. */\nexport type ConversationHandler<Opening> = (opening: Opening, conversation: Conversation) => void\n\n/** Starts the host, once, the first time a conversation needs it. */\nexport type StartHost<Opening> = () =>\n | ConversationHandler<Opening>\n | Promise<ConversationHandler<Opening>>\n\n/** Where `SharedHost` runs the host: in a SharedWorker, or in the page when there is none. */\nexport interface SharedHostOptions<Opening> {\n /**\n * The SharedWorker whose entry calls `serve`. Constructed by the\n * application, since a bundler finds a worker only in a literal\n * `new SharedWorker(new URL('./worker.ts', import.meta.url), …)`.\n */\n readonly worker: () => { readonly port: MessagePort }\n /** The host, for a browser without SharedWorker: started in the top document. */\n readonly inPage: StartHost<Opening>\n}\n\n/** A page's way to the host. */\nexport interface SharedHostConnection<Opening> {\n /** Opens a conversation for `opening`, returning the page's end of it. */\n readonly open: (opening: Opening) => MessagePort\n}\n\nexport interface SharedHost<Opening> {\n readonly name: string\n /** A SharedWorker's entry: hands the host every conversation a page opens. */\n readonly serve: (start: StartHost<Opening>) => void\n /**\n * A page's connection: through the SharedWorker, or, without one, to the\n * host in the top document. Call it in the top document too when that\n * document frames the pages, so that it is the one the frames meet.\n */\n readonly connect: (options: SharedHostOptions<Opening>) => SharedHostConnection<Opening>\n}\n\n/**\n * A shared host: its `name`, which keeps two on one origin apart, and\n * the schema of what opens a conversation, which the host decodes before\n * anything else, ignoring a message that does not decode.\n */\nconst define = <Opening, Encoded>(options: {\n readonly name: string\n readonly opening: Schema.Codec<Opening, Encoded, never, never>\n}): SharedHost<Opening> => {\n const { name } = options\n const Envelope = Schema.Struct({\n host: Schema.Literal(name),\n conversation: Schema.String,\n opening: options.opening,\n })\n const encode = Schema.encodeSync(Envelope)\n const decode = Schema.decodeUnknownOption(Envelope)\n const lockOf = (conversation: string) => `foldkit-shared-host:${name}:${conversation}`\n const marker = `foldkit-shared-host:${name}`\n\n const receive = (\n host: () => Promise<ConversationHandler<Opening>>,\n data: unknown,\n port: MessagePort | undefined,\n ) =>\n Option.match(\n Option.zipWith(decode(data), Option.fromUndefinedOr(port), (envelope, given) => ({\n envelope,\n given,\n })),\n {\n // Not a page's opening, or one without its port: nothing to answer.\n onNone: () => {},\n onSome: ({ envelope, given }) => {\n const ended = new AbortController()\n // The page holds this lock while it lives, so the host is granted it\n // when the page's document unloads or crashes.\n Option.match(locks(), {\n onNone: () => {},\n onSome: held => {\n void held.request(lockOf(envelope.conversation), () => ended.abort())\n },\n })\n void host()\n .then(started => started(envelope.opening, { port: given, signal: ended.signal }))\n .catch(reportError)\n },\n },\n )\n\n const once = (start: StartHost<Opening>): (() => Promise<ConversationHandler<Opening>>) => {\n let started: Promise<ConversationHandler<Opening>> | undefined\n return () => (started ??= Promise.resolve().then(start))\n }\n\n const serve = (start: StartHost<Opening>): void => {\n const host = once(start)\n self.addEventListener('connect', event => {\n if (!(event instanceof MessageEvent)) return\n const [connection] = event.ports\n connection?.addEventListener('message', message =>\n receive(host, message.data, message.ports[0]),\n )\n connection?.start()\n })\n }\n\n const connect = ({\n worker,\n inPage,\n }: SharedHostOptions<Opening>): SharedHostConnection<Opening> => {\n const send = routeOf(worker, inPage)\n return {\n open: opening => {\n const { port1, port2 } = new MessageChannel()\n const conversation = crypto.randomUUID()\n const envelope = encode({ host: name, conversation, opening })\n // The opening goes once the lock is held, so the host cannot be\n // granted it first and end the conversation before it began.\n void hold(lockOf(conversation)).then(() => send(envelope, port2))\n return port1\n },\n }\n }\n\n const routeOf = (\n worker: SharedHostOptions<Opening>['worker'],\n inPage: StartHost<Opening>,\n ): ((envelope: unknown, port: MessagePort) => void) => {\n if (typeof SharedWorker !== 'undefined') {\n const { port } = worker()\n return (envelope, given) => port.postMessage(envelope, [given])\n }\n return Option.match(acceptingTop(marker), {\n onSome: top => (envelope, given) => top.postMessage(envelope, location.origin, [given]),\n onNone: () => {\n const host = once(inPage)\n Reflect.set(window, marker, true)\n window.addEventListener('message', event => {\n if (event.origin === location.origin) receive(host, event.data, event.ports[0])\n })\n return (envelope, given) => receive(host, envelope, given)\n },\n })\n }\n\n return { name, serve, connect }\n}\n\n/** The top document, when it is another of this origin's and runs this host. */\nconst acceptingTop = (marker: string): Option.Option<Window> => {\n const top = window.top\n if (top === null || top === window) return Option.none()\n try {\n return Reflect.get(top, marker) === true ? Option.some(top) : Option.none()\n } catch {\n // Another origin's top document: reading it throws.\n return Option.none()\n }\n}\n\n/** Resolves once this document holds `lock`, which it then holds while it lives. */\nconst hold = (lock: string): Promise<void> =>\n Option.match(locks(), {\n onNone: () => Promise.resolve(),\n onSome: held =>\n new Promise(granted => {\n void held.request(lock, () => {\n granted()\n return new Promise(() => {})\n })\n }),\n })\n\n/** Web Locks, which a page outside a secure context, or an older browser, lacks. */\nconst locks = (): Option.Option<LockManager> => Option.fromUndefinedOr(navigator.locks)\n\n/**\n * One server in the browser for many documents: every tab and frame of an\n * application meets the same host. A page opens conversations, each a\n * `MessagePort` with a decoded opening that says what it is for; the host, in\n * a SharedWorker shared by every tab, or in the top document where there is no\n * SharedWorker, is handed each one with a signal that aborts when the page\n * that opened it is gone.\n *\n * It owns the host's lifetime and the routing of conversations, nothing\n * spoken over them: Sync's `portSocket` and Remote's `port` do that.\n */\nexport const SharedHost = { define }\n"],"mappings":";;;;;;;AAmDA,MAAM,UAA4B,YAGP;CACzB,MAAM,EAAE,SAAS;CACjB,MAAM,WAAW,OAAO,OAAO;EAC7B,MAAM,OAAO,QAAQ,IAAI;EACzB,cAAc,OAAO;EACrB,SAAS,QAAQ;CACnB,CAAC;CACD,MAAM,SAAS,OAAO,WAAW,QAAQ;CACzC,MAAM,SAAS,OAAO,oBAAoB,QAAQ;CAClD,MAAM,UAAU,iBAAyB,uBAAuB,KAAK,GAAG;CACxE,MAAM,SAAS,uBAAuB;CAEtC,MAAM,WACJ,MACA,MACA,SAEA,OAAO,MACL,OAAO,QAAQ,OAAO,IAAI,GAAG,OAAO,gBAAgB,IAAI,IAAI,UAAU,WAAW;EAC/E;EACA;CACF,EAAE,GACF;EAEE,cAAc,CAAC;EACf,SAAS,EAAE,UAAU,YAAY;GAC/B,MAAM,QAAQ,IAAI,gBAAgB;GAGlC,OAAO,MAAM,MAAM,GAAG;IACpB,cAAc,CAAC;IACf,SAAQ,SAAQ;KACd,KAAU,QAAQ,OAAO,SAAS,YAAY,SAAS,MAAM,MAAM,CAAC;IACtE;GACF,CAAC;GACD,KAAU,CAAC,CACR,MAAK,YAAW,QAAQ,SAAS,SAAS;IAAE,MAAM;IAAO,QAAQ,MAAM;GAAO,CAAC,CAAC,CAAC,CACjF,MAAM,WAAW;EACtB;CACF,CACF;CAEF,MAAM,QAAQ,UAA6E;EACzF,IAAI;EACJ,aAAc,YAAY,QAAQ,QAAQ,CAAC,CAAC,KAAK,KAAK;CACxD;CAEA,MAAM,SAAS,UAAoC;EACjD,MAAM,OAAO,KAAK,KAAK;EACvB,KAAK,iBAAiB,YAAW,UAAS;GACxC,IAAI,EAAE,iBAAiB,eAAe;GACtC,MAAM,CAAC,cAAc,MAAM;GAC3B,YAAY,iBAAiB,YAAW,YACtC,QAAQ,MAAM,QAAQ,MAAM,QAAQ,MAAM,EAAE,CAC9C;GACA,YAAY,MAAM;EACpB,CAAC;CACH;CAEA,MAAM,WAAW,EACf,QACA,aAC+D;EAC/D,MAAM,OAAO,QAAQ,QAAQ,MAAM;EACnC,OAAO,EACL,OAAM,YAAW;GACf,MAAM,EAAE,OAAO,UAAU,IAAI,eAAe;GAC5C,MAAM,eAAe,OAAO,WAAW;GACvC,MAAM,WAAW,OAAO;IAAE,MAAM;IAAM;IAAc;GAAQ,CAAC;GAG7D,KAAU,OAAO,YAAY,CAAC,CAAC,CAAC,WAAW,KAAK,UAAU,KAAK,CAAC;GAChE,OAAO;EACT,EACF;CACF;CAEA,MAAM,WACJ,QACA,WACqD;EACrD,IAAI,OAAO,iBAAiB,aAAa;GACvC,MAAM,EAAE,SAAS,OAAO;GACxB,QAAQ,UAAU,UAAU,KAAK,YAAY,UAAU,CAAC,KAAK,CAAC;EAChE;EACA,OAAO,OAAO,MAAM,aAAa,MAAM,GAAG;GACxC,SAAQ,SAAQ,UAAU,UAAU,IAAI,YAAY,UAAU,SAAS,QAAQ,CAAC,KAAK,CAAC;GACtF,cAAc;IACZ,MAAM,OAAO,KAAK,MAAM;IACxB,QAAQ,IAAI,QAAQ,QAAQ,IAAI;IAChC,OAAO,iBAAiB,YAAW,UAAS;KAC1C,IAAI,MAAM,WAAW,SAAS,QAAQ,QAAQ,MAAM,MAAM,MAAM,MAAM,MAAM,EAAE;IAChF,CAAC;IACD,QAAQ,UAAU,UAAU,QAAQ,MAAM,UAAU,KAAK;GAC3D;EACF,CAAC;CACH;CAEA,OAAO;EAAE;EAAM;EAAO;CAAQ;AAChC;;AAGA,MAAM,gBAAgB,WAA0C;CAC9D,MAAM,MAAM,OAAO;CACnB,IAAI,QAAQ,QAAQ,QAAQ,QAAQ,OAAO,OAAO,KAAK;CACvD,IAAI;EACF,OAAO,QAAQ,IAAI,KAAK,MAAM,MAAM,OAAO,OAAO,KAAK,GAAG,IAAI,OAAO,KAAK;CAC5E,QAAQ;EAEN,OAAO,OAAO,KAAK;CACrB;AACF;;AAGA,MAAM,QAAQ,SACZ,OAAO,MAAM,MAAM,GAAG;CACpB,cAAc,QAAQ,QAAQ;CAC9B,SAAQ,SACN,IAAI,SAAQ,YAAW;EACrB,KAAU,QAAQ,YAAY;GAC5B,QAAQ;GACR,OAAO,IAAI,cAAc,CAAC,CAAC;EAC7B,CAAC;CACH,CAAC;AACL,CAAC;;AAGH,MAAM,cAA0C,OAAO,gBAAgB,UAAU,KAAK;;;;;;;;;;;;AAatF,MAAa,aAAa,EAAE,OAAO"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Option, Queue, Schema } from "effect";
|
|
2
|
+
import { Bundle } from "foldkit-bundle";
|
|
3
|
+
import * as ManagedResource from "foldkit/managedResource";
|
|
4
|
+
/** The slice of an EventSource the bundle needs; satisfied by the platform class. */
|
|
5
|
+
export interface SourceHandle {
|
|
6
|
+
readonly readyState: number;
|
|
7
|
+
close(): void;
|
|
8
|
+
onopen: ((event: any) => void) | null;
|
|
9
|
+
onmessage: ((event: any) => void) | null;
|
|
10
|
+
onerror: ((event: any) => void) | null;
|
|
11
|
+
}
|
|
12
|
+
export declare const SseModel: Schema.Struct<{
|
|
13
|
+
readonly url: Schema.String;
|
|
14
|
+
readonly status: Schema.Literals<readonly ["closed", "connecting", "open"]>;
|
|
15
|
+
readonly lastError: Schema.OptionFromNullOr<Schema.String>;
|
|
16
|
+
}>;
|
|
17
|
+
export type SseModel = typeof SseModel.Type;
|
|
18
|
+
export declare const SseMessage: import("foldkit/message").MessageUnion<{
|
|
19
|
+
readonly Connecting: {};
|
|
20
|
+
readonly Opened: {};
|
|
21
|
+
/** Notifies without storing: project the payload into your own field to keep it. */
|
|
22
|
+
readonly Received: {
|
|
23
|
+
data: Schema.String;
|
|
24
|
+
};
|
|
25
|
+
readonly Closed: {};
|
|
26
|
+
readonly Failed: {
|
|
27
|
+
message: Schema.String;
|
|
28
|
+
};
|
|
29
|
+
}>;
|
|
30
|
+
export type SseMessage = typeof SseMessage.Type;
|
|
31
|
+
/**
|
|
32
|
+
* What the reader sees: the stream's state, or its error. A page derives
|
|
33
|
+
* this instead of keeping its own connection state machine. `lastError`
|
|
34
|
+
* carries the stored failure text: the runtime error sentence (`event stream
|
|
35
|
+
* error for …`) or the acquire error's own message.
|
|
36
|
+
*/
|
|
37
|
+
export declare const SseView: import("foldkit/schema").TaggedUnion<{
|
|
38
|
+
readonly Disconnected: {};
|
|
39
|
+
readonly Connecting: {};
|
|
40
|
+
readonly Connected: {};
|
|
41
|
+
readonly Error: {
|
|
42
|
+
error: Schema.String;
|
|
43
|
+
};
|
|
44
|
+
}>;
|
|
45
|
+
export type SseView = typeof SseView.Type;
|
|
46
|
+
/**
|
|
47
|
+
* Whether the stream is live: open with no error outstanding. The browser
|
|
48
|
+
* reconnects a dropped stream itself; a `Failed` only records the error.
|
|
49
|
+
*/
|
|
50
|
+
export declare const isLive: (model: SseModel) => boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The view of a stream, and whether its page still wants it. An error stands
|
|
53
|
+
* until the page stops wanting the stream; a retry still waiting for its
|
|
54
|
+
* stream reads as connecting, so it answers at once.
|
|
55
|
+
*/
|
|
56
|
+
export declare const viewOfSse: (model: SseModel, wanted: boolean) => SseView;
|
|
57
|
+
/** The registry value behind the source tag: the live source and its incoming queue. */
|
|
58
|
+
export interface AcquiredSource {
|
|
59
|
+
readonly source: SourceHandle;
|
|
60
|
+
readonly events: Queue.Queue<SseMessage>;
|
|
61
|
+
}
|
|
62
|
+
/** The live stream, for the incoming subscription. One assembly holds one. */
|
|
63
|
+
export declare const Source: ManagedResource.ManagedResource<AcquiredSource, ManagedResource.ManagedResourceService<"sse">>;
|
|
64
|
+
/** The source service a placed SSE stream requires. */
|
|
65
|
+
export type SourceService = ManagedResource.ServiceOf<typeof Source>;
|
|
66
|
+
/**
|
|
67
|
+
* Server-sent events as a bundle: connection lifecycle in the Model, the
|
|
68
|
+
* event stream through one Managed Resource. One-directional, so there is no
|
|
69
|
+
* send: the server speaks, the Model listens. The browser reconnects dropped
|
|
70
|
+
* streams itself; a Failed only records the error.
|
|
71
|
+
*/
|
|
72
|
+
export declare const sse: <const Name extends string>(config: {
|
|
73
|
+
readonly name: Name;
|
|
74
|
+
/** Defaults to the platform EventSource, read lazily so tests can substitute a double. */
|
|
75
|
+
readonly createSource?: ((url: string) => SourceHandle) | undefined;
|
|
76
|
+
}) => Bundle.Bundle<Name, {
|
|
77
|
+
readonly url: string;
|
|
78
|
+
}, {
|
|
79
|
+
readonly url: string;
|
|
80
|
+
readonly status: "closed" | "connecting" | "open";
|
|
81
|
+
readonly lastError: Option.Option<string>;
|
|
82
|
+
}, {
|
|
83
|
+
readonly _tag: "Connecting";
|
|
84
|
+
} | {
|
|
85
|
+
readonly _tag: "Closed";
|
|
86
|
+
} | {
|
|
87
|
+
readonly _tag: "Failed";
|
|
88
|
+
readonly message: string;
|
|
89
|
+
} | {
|
|
90
|
+
readonly _tag: "Opened";
|
|
91
|
+
} | {
|
|
92
|
+
readonly _tag: "Received";
|
|
93
|
+
readonly data: string;
|
|
94
|
+
}, never, ManagedResource.ManagedResourceService<"sse">, ManagedResource.ManagedResourceService<"sse">, void, {
|
|
95
|
+
source: ManagedResource.Entry<{
|
|
96
|
+
readonly url: string;
|
|
97
|
+
readonly status: "closed" | "connecting" | "open";
|
|
98
|
+
readonly lastError: Option.Option<string>;
|
|
99
|
+
}, {
|
|
100
|
+
readonly _tag: "Connecting";
|
|
101
|
+
} | {
|
|
102
|
+
readonly _tag: "Closed";
|
|
103
|
+
} | {
|
|
104
|
+
readonly _tag: "Failed";
|
|
105
|
+
readonly message: string;
|
|
106
|
+
} | {
|
|
107
|
+
readonly _tag: "Opened";
|
|
108
|
+
} | {
|
|
109
|
+
readonly _tag: "Received";
|
|
110
|
+
readonly data: string;
|
|
111
|
+
}, Option.Option<string>, AcquiredSource, ManagedResource.ManagedResourceService<"sse">, () => {
|
|
112
|
+
readonly _tag: "Connecting";
|
|
113
|
+
}>;
|
|
114
|
+
}, {}>;
|
|
115
|
+
//# sourceMappingURL=sse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.d.ts","names":[],"sources":["../../src/net/sse.ts"],"mappings":";;;;iBAUiB;WACN;EACT;EACA,UAAU;EACV,aAAa;EACb,WAAW;;qBAGA,UAAQ,OAAA;;;;;YAKT,kBAAkB,SAAS;qBAE1B,sCAAU;;;;;IAIT,MAAI,OAAA;;;;IAEN,SAAO,OAAA;;;YAEP,oBAAoB,WAAW;;;;;;;qBAQ9B,kCAAO;;;;;IAIT,OAAK,OAAA;;;YAEJ,iBAAiB,QAAQ;;;;;qBAMxB,SAAM,OAAW;;;;;;qBAQjB,YAAS,OAAW,UAAQ,oBAAoB;;iBAY5C;WACN,QAAQ;WACR,QAAQ,MAAM,MAAM;;;qBAIlB,QAAM,gBAAA,gBAAA,gBAAA,gBAAA;;YAGP,gBAAgB,gBAAgB,iBAAiB;;;;;;;qBAWhD,YAAa,qBAAmB;WAClC,MAAM;;WAEN,iBAAiB,gBAAgB;MAC3C,OAAA,OAAA"}
|
package/dist/net/sse.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
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/sse.ts
|
|
9
|
+
const SseModel = Schema.Struct({
|
|
10
|
+
url: Schema.String,
|
|
11
|
+
status: Schema.Literals([
|
|
12
|
+
"closed",
|
|
13
|
+
"connecting",
|
|
14
|
+
"open"
|
|
15
|
+
]),
|
|
16
|
+
lastError: Schema.OptionFromNullOr(Schema.String)
|
|
17
|
+
});
|
|
18
|
+
const SseMessage = defineMessageUnion({
|
|
19
|
+
Connecting: {},
|
|
20
|
+
Opened: {},
|
|
21
|
+
/** Notifies without storing: project the payload into your own field to keep it. */
|
|
22
|
+
Received: { data: Schema.String },
|
|
23
|
+
Closed: {},
|
|
24
|
+
Failed: { message: Schema.String }
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* What the reader sees: the stream's state, or its error. A page derives
|
|
28
|
+
* this instead of keeping its own connection state machine. `lastError`
|
|
29
|
+
* carries the stored failure text: the runtime error sentence (`event stream
|
|
30
|
+
* error for …`) or the acquire error's own message.
|
|
31
|
+
*/
|
|
32
|
+
const SseView = defineTaggedUnion({
|
|
33
|
+
Disconnected: {},
|
|
34
|
+
Connecting: {},
|
|
35
|
+
Connected: {},
|
|
36
|
+
Error: { error: Schema.String }
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* Whether the stream is live: open with no error outstanding. The browser
|
|
40
|
+
* reconnects a dropped stream itself; a `Failed` only records the error.
|
|
41
|
+
*/
|
|
42
|
+
const isLive = (model) => model.status === "open" && Option.isNone(model.lastError);
|
|
43
|
+
/**
|
|
44
|
+
* The view of a stream, and whether its page still wants it. An error stands
|
|
45
|
+
* until the page stops wanting the stream; a retry still waiting for its
|
|
46
|
+
* stream reads as connecting, so it answers at once.
|
|
47
|
+
*/
|
|
48
|
+
const viewOfSse = (model, wanted) => Option.match(model.lastError, {
|
|
49
|
+
onSome: (error) => model.status === "open" || !wanted ? SseView.Error({ error }) : SseView.Connecting(),
|
|
50
|
+
onNone: () => {
|
|
51
|
+
if (model.status === "open") return SseView.Connected();
|
|
52
|
+
if (model.status === "connecting" || wanted) return SseView.Connecting();
|
|
53
|
+
return SseView.Disconnected();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
/** The live stream, for the incoming subscription. One assembly holds one. */
|
|
57
|
+
const Source = ManagedResource.tag()("sse");
|
|
58
|
+
const failMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
59
|
+
/**
|
|
60
|
+
* Server-sent events as a bundle: connection lifecycle in the Model, the
|
|
61
|
+
* event stream through one Managed Resource. One-directional, so there is no
|
|
62
|
+
* send: the server speaks, the Model listens. The browser reconnects dropped
|
|
63
|
+
* streams itself; a Failed only records the error.
|
|
64
|
+
*/
|
|
65
|
+
const sse = (config) => {
|
|
66
|
+
const createSource = config.createSource ?? ((url) => new EventSource(url));
|
|
67
|
+
return Bundle.make(config.name, {
|
|
68
|
+
Model: SseModel,
|
|
69
|
+
Message: SseMessage,
|
|
70
|
+
args: Schema.Struct({ url: Schema.String }),
|
|
71
|
+
init: (args) => ({ model: {
|
|
72
|
+
url: args.url,
|
|
73
|
+
status: "closed",
|
|
74
|
+
lastError: Option.none()
|
|
75
|
+
} }),
|
|
76
|
+
update: (model, message) => SseMessage.match(message, {
|
|
77
|
+
Connecting: () => ({ model: unlessSame(model, {
|
|
78
|
+
...model,
|
|
79
|
+
status: "connecting"
|
|
80
|
+
}) }),
|
|
81
|
+
Opened: () => ({ model: unlessSame(model, {
|
|
82
|
+
...model,
|
|
83
|
+
status: "open",
|
|
84
|
+
lastError: Option.none()
|
|
85
|
+
}) }),
|
|
86
|
+
Received: () => ({ model }),
|
|
87
|
+
Closed: () => ({ model: unlessSame(model, {
|
|
88
|
+
...model,
|
|
89
|
+
status: "closed"
|
|
90
|
+
}) }),
|
|
91
|
+
Failed: ({ message }) => ({ model: unlessSame(model, {
|
|
92
|
+
...model,
|
|
93
|
+
status: "connecting",
|
|
94
|
+
lastError: Option.some(message)
|
|
95
|
+
}) })
|
|
96
|
+
}),
|
|
97
|
+
resources: () => ManagedResource.make()((entry) => ({ source: entry(Schema.Option(Schema.String), {
|
|
98
|
+
resource: Source,
|
|
99
|
+
modelToMaybeRequirements: (model) => Option.some(model.url),
|
|
100
|
+
acquire: (url) => Effect.gen(function* () {
|
|
101
|
+
const events = yield* Queue.unbounded();
|
|
102
|
+
const source = createSource(url);
|
|
103
|
+
let tail = Promise.resolve();
|
|
104
|
+
const enqueue = (effect) => {
|
|
105
|
+
const run = () => Effect.runPromise(effect).then(() => void 0, () => void 0);
|
|
106
|
+
tail = tail.then(run, run);
|
|
107
|
+
};
|
|
108
|
+
const offer = (message) => enqueue(Queue.offer(events, message));
|
|
109
|
+
source.onopen = () => offer(SseMessage.Opened());
|
|
110
|
+
source.onmessage = (event) => offer(SseMessage.Received({ data: String(event.data) }));
|
|
111
|
+
source.onerror = () => offer(SseMessage.Failed({ message: `event stream error for ${url}` }));
|
|
112
|
+
return {
|
|
113
|
+
source,
|
|
114
|
+
events
|
|
115
|
+
};
|
|
116
|
+
}),
|
|
117
|
+
onAcquired: () => SseMessage.Connecting(),
|
|
118
|
+
onReleased: () => SseMessage.Closed(),
|
|
119
|
+
onAcquireError: (error) => SseMessage.Failed({ message: failMessage(error) }),
|
|
120
|
+
release: ({ source, events }) => Effect.asVoid(Effect.andThen(Effect.sync(() => {
|
|
121
|
+
source.onopen = null;
|
|
122
|
+
source.onmessage = null;
|
|
123
|
+
source.onerror = null;
|
|
124
|
+
source.close();
|
|
125
|
+
}), Queue.shutdown(events)))
|
|
126
|
+
}) })),
|
|
127
|
+
subscriptions: () => Subscription.make()((entry) => ({ incoming: entry({ live: Schema.Boolean }, {
|
|
128
|
+
modelToDependencies: (model) => ({ live: model.status !== "closed" }),
|
|
129
|
+
dependenciesToStream: ({ live }) => !live ? Stream.empty : Stream.unwrap(Effect.matchEffect(Source.get, {
|
|
130
|
+
onFailure: () => Effect.succeed(Stream.empty),
|
|
131
|
+
onSuccess: ({ source, events }) => Effect.succeed(Stream.concat(source.readyState === 1 ? Stream.make(SseMessage.Opened()) : Stream.empty, Stream.fromQueue(events)))
|
|
132
|
+
}))
|
|
133
|
+
}) }))
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
export { Source, SseMessage, SseModel, SseView, isLive, sse, viewOfSse };
|
|
137
|
+
|
|
138
|
+
//# sourceMappingURL=sse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.js","names":[],"sources":["../../src/net/sse.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 an EventSource the bundle needs; satisfied by the platform class. */\nexport interface SourceHandle {\n readonly readyState: number\n close(): void\n onopen: ((event: any) => void) | null\n onmessage: ((event: any) => void) | null\n onerror: ((event: any) => void) | null\n}\n\nexport const SseModel = Schema.Struct({\n url: Schema.String,\n status: Schema.Literals(['closed', 'connecting', 'open']),\n lastError: Schema.OptionFromNullOr(Schema.String),\n})\nexport type SseModel = typeof SseModel.Type\n\nexport const SseMessage = 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})\nexport type SseMessage = typeof SseMessage.Type\n\n/**\n * What the reader sees: the stream's state, or its error. A page derives\n * this instead of keeping its own connection state machine. `lastError`\n * carries the stored failure text: the runtime error sentence (`event stream\n * error for …`) or the acquire error's own message.\n */\nexport const SseView = defineTaggedUnion({\n Disconnected: {},\n Connecting: {},\n Connected: {},\n Error: { error: Schema.String },\n})\nexport type SseView = typeof SseView.Type\n\n/**\n * Whether the stream is live: open with no error outstanding. The browser\n * reconnects a dropped stream itself; a `Failed` only records the error.\n */\nexport const isLive = (model: SseModel): boolean =>\n model.status === 'open' && Option.isNone(model.lastError)\n\n/**\n * The view of a stream, and whether its page still wants it. An error stands\n * until the page stops wanting the stream; a retry still waiting for its\n * stream reads as connecting, so it answers at once.\n */\nexport const viewOfSse = (model: SseModel, wanted: boolean): SseView =>\n Option.match(model.lastError, {\n onSome: error =>\n model.status === 'open' || !wanted ? SseView.Error({ error }) : SseView.Connecting(),\n onNone: () => {\n if (model.status === 'open') return SseView.Connected()\n if (model.status === 'connecting' || wanted) return SseView.Connecting()\n return SseView.Disconnected()\n },\n })\n\n/** The registry value behind the source tag: the live source and its incoming queue. */\nexport interface AcquiredSource {\n readonly source: SourceHandle\n readonly events: Queue.Queue<SseMessage>\n}\n\n/** The live stream, for the incoming subscription. One assembly holds one. */\nexport const Source = ManagedResource.tag<AcquiredSource>()('sse')\n\n/** The source service a placed SSE stream requires. */\nexport type SourceService = ManagedResource.ServiceOf<typeof Source>\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\n/**\n * Server-sent events as a bundle: connection lifecycle in the Model, the\n * event stream through one Managed Resource. One-directional, so there is no\n * send: the server speaks, the Model listens. The browser reconnects dropped\n * streams itself; a Failed only records the error.\n */\nexport const sse = <const Name extends string>(config: {\n readonly name: Name\n /** Defaults to the platform EventSource, read lazily so tests can substitute a double. */\n readonly createSource?: ((url: string) => SourceHandle) | undefined\n}) => {\n const createSource: (url: string) => SourceHandle =\n config.createSource ?? (url => new EventSource(url) as unknown as SourceHandle)\n return Bundle.make(config.name, {\n Model: SseModel,\n Message: SseMessage,\n args: Schema.Struct({ url: Schema.String }),\n init: (args): Update.Return<SseModel, SseMessage, SourceService> => ({\n model: { url: args.url, status: 'closed', lastError: Option.none() },\n }),\n update: (\n model,\n message,\n ): Update.ReturnWithOutMessage<SseModel, SseMessage, never, SourceService> =>\n SseMessage.match(message, {\n Connecting: () => ({ model: unlessSame(model, { ...model, status: 'connecting' }) }),\n Opened: () => ({\n model: unlessSame(model, { ...model, status: 'open', lastError: Option.none() }),\n }),\n Received: () => ({ model }),\n Closed: () => ({ model: unlessSame(model, { ...model, status: 'closed' }) }),\n // EventSource retries on its own, erroring the same way on every attempt.\n Failed: ({ message }) => ({\n model: unlessSame(model, {\n ...model,\n status: 'connecting',\n lastError: Option.some(message),\n }),\n }),\n }),\n resources: () =>\n ManagedResource.make<SseModel, SseMessage>()(entry => ({\n source: entry(Schema.Option(Schema.String), {\n resource: Source,\n modelToMaybeRequirements: model => Option.some(model.url),\n acquire: (url: string) =>\n Effect.gen(function* () {\n const events = yield* Queue.unbounded<SseMessage>()\n const source = createSource(url)\n // Offers run in arrival order; see the WebSocket bundle.\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: SseMessage) => enqueue(Queue.offer(events, message))\n source.onopen = () => offer(SseMessage.Opened())\n // Event data is always text per the SSE spec.\n source.onmessage = event => offer(SseMessage.Received({ data: String(event.data) }))\n source.onerror = () =>\n offer(SseMessage.Failed({ message: `event stream error for ${url}` }))\n return { source, events }\n }),\n onAcquired: () => SseMessage.Connecting(),\n onReleased: () => SseMessage.Closed(),\n onAcquireError: error => SseMessage.Failed({ message: failMessage(error) }),\n // Daemon teardown: close the stream and drain the queue.\n release: ({ source, events }) =>\n Effect.asVoid(\n Effect.andThen(\n Effect.sync(() => {\n source.onopen = null\n source.onmessage = null\n source.onerror = null\n source.close()\n }),\n Queue.shutdown(events),\n ),\n ),\n }),\n })),\n subscriptions: (): Subscription.Subscriptions<SseModel, SseMessage, SourceService> =>\n Subscription.make<SseModel, SseMessage, SourceService>()(entry => ({\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(Source.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: ({ source, events }) =>\n Effect.succeed(\n Stream.concat(\n // A fast server opens before the stream attaches.\n // 1 is OPEN for EventSource everywhere.\n source.readyState === 1\n ? Stream.make(SseMessage.Opened())\n : Stream.empty,\n Stream.fromQueue(events),\n ),\n ),\n }),\n ),\n },\n ),\n })),\n })\n}\n"],"mappings":"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"}
|