foldkit-primitives 0.2.1 → 0.4.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.
Files changed (57) hide show
  1. package/README.md +514 -43
  2. package/dist/device/index.d.mts +11 -11
  3. package/dist/device/index.d.mts.map +1 -1
  4. package/dist/device/index.mjs +95 -87
  5. package/dist/device/index.mjs.map +1 -1
  6. package/dist/dom/index.d.mts +71 -1
  7. package/dist/dom/index.d.mts.map +1 -1
  8. package/dist/dom/index.mjs +303 -5
  9. package/dist/dom/index.mjs.map +1 -1
  10. package/dist/editable-text-CKkS_mWt.d.mts +235 -0
  11. package/dist/editable-text-CKkS_mWt.d.mts.map +1 -0
  12. package/dist/events/index.d.mts +10 -1
  13. package/dist/events/index.d.mts.map +1 -1
  14. package/dist/events/index.mjs +34 -18
  15. package/dist/events/index.mjs.map +1 -1
  16. package/dist/follow-tab-stop-DpxwLXvU.mjs +800 -0
  17. package/dist/follow-tab-stop-DpxwLXvU.mjs.map +1 -0
  18. package/dist/index-Cppf-lBx.d.mts +130 -0
  19. package/dist/index-Cppf-lBx.d.mts.map +1 -0
  20. package/dist/index.d.mts +8 -6
  21. package/dist/index.mjs +8 -6
  22. package/dist/input-modality-DGOhAknO.d.mts +26 -0
  23. package/dist/input-modality-DGOhAknO.d.mts.map +1 -0
  24. package/dist/input-modality-m6v7__Xs.mjs +43 -0
  25. package/dist/input-modality-m6v7__Xs.mjs.map +1 -0
  26. package/dist/interaction/index.d.mts +1017 -0
  27. package/dist/interaction/index.d.mts.map +1 -0
  28. package/dist/interaction/index.mjs +1647 -0
  29. package/dist/interaction/index.mjs.map +1 -0
  30. package/dist/internal-DP-xLKbT.mjs +37 -0
  31. package/dist/internal-DP-xLKbT.mjs.map +1 -0
  32. package/dist/media/index.d.mts.map +1 -1
  33. package/dist/media/index.mjs +3 -3
  34. package/dist/media/index.mjs.map +1 -1
  35. package/dist/motion/index.d.mts +2 -112
  36. package/dist/motion/index.mjs +2 -208
  37. package/dist/motion-DGGBZ0wt.mjs +240 -0
  38. package/dist/motion-DGGBZ0wt.mjs.map +1 -0
  39. package/dist/net/index.d.mts +68 -4
  40. package/dist/net/index.d.mts.map +1 -1
  41. package/dist/net/index.mjs +139 -56
  42. package/dist/net/index.mjs.map +1 -1
  43. package/dist/observers/index.d.mts +1 -1
  44. package/dist/observers/index.mjs +3 -0
  45. package/dist/observers/index.mjs.map +1 -1
  46. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  47. package/dist/state/index.d.mts +57 -15
  48. package/dist/state/index.d.mts.map +1 -1
  49. package/dist/state/index.mjs +198 -90
  50. package/dist/state/index.mjs.map +1 -1
  51. package/dist/time/index.d.mts +21 -1
  52. package/dist/time/index.d.mts.map +1 -1
  53. package/dist/time/index.mjs +48 -19
  54. package/dist/time/index.mjs.map +1 -1
  55. package/package.json +31 -8
  56. package/dist/motion/index.d.mts.map +0 -1
  57. package/dist/motion/index.mjs.map +0 -1
@@ -1,8 +1,10 @@
1
+ import { i as unlessSame } from "../internal-DP-xLKbT.mjs";
1
2
  import { Effect, Option, Queue, Schema, Stream } from "effect";
2
3
  import { defineMessageUnion } from "foldkit/message";
3
4
  import * as Subscription from "foldkit/subscription";
4
5
  import { Bundle } from "foldkit-bundle";
5
6
  import * as ManagedResource from "foldkit/managedResource";
7
+ import { defineTaggedUnion } from "foldkit/schema";
6
8
  //#region src/net/broadcast.ts
7
9
  /**
8
10
  * Cross-tab messaging as an entry plus a Command — not a bundle, because the
@@ -86,7 +88,7 @@ const Online = Bundle.make("Online", {
86
88
  Model: OnlineModel,
87
89
  Message: OnlineMessage,
88
90
  init: () => ({ model: { online: typeof navigator !== "undefined" && typeof navigator.onLine === "boolean" ? navigator.onLine : true } }),
89
- update: (model, message) => ({ model: { online: message.online } }),
91
+ update: (model, message) => message.online === model.online ? { model } : { model: { online: message.online } },
90
92
  subscriptions: () => Subscription.make()(() => ({ changes: Subscription.persistent(presenceStream()) }))
91
93
  });
92
94
  //#endregion
@@ -114,6 +116,34 @@ const SseMessage = defineMessageUnion({
114
116
  Closed: {},
115
117
  Failed: { message: Schema.String }
116
118
  });
119
+ /**
120
+ * What the reader sees: the stream's state, or its error. A page derives
121
+ * this instead of keeping its own connection state machine. `lastError`
122
+ * carries the stored failure text: the runtime error sentence (`event stream
123
+ * error for …`) or the acquire error's own message.
124
+ */
125
+ const SseView = defineTaggedUnion({
126
+ Disconnected: {},
127
+ Connecting: {},
128
+ Connected: {},
129
+ Error: { error: Schema.String }
130
+ });
131
+ /**
132
+ * Whether the stream is live: open with no error outstanding. The browser
133
+ * reconnects a dropped stream itself; a `Failed` only records the error.
134
+ */
135
+ const isLive = (model) => model.status === "open" && model.lastError === null;
136
+ /**
137
+ * The view of a stream, and whether its page still wants it. An error stands
138
+ * until the page stops wanting the stream; a retry still waiting for its
139
+ * stream reads as connecting, so it answers at once.
140
+ */
141
+ const viewOfSse = (model, wanted) => {
142
+ if (model.lastError !== null) return model.status === "open" || !wanted ? SseView.Error({ error: model.lastError }) : SseView.Connecting();
143
+ if (model.status === "open") return SseView.Connected();
144
+ if (model.status === "connecting" || wanted) return SseView.Connecting();
145
+ return SseView.Disconnected();
146
+ };
117
147
  /** The live stream, for the incoming subscription. One assembly holds one. */
118
148
  const Source = ManagedResource.tag()("sse");
119
149
  const failMessage$1 = (error) => error instanceof Error ? error.message : String(error);
@@ -129,25 +159,25 @@ const sse = (config) => {
129
159
  lastError: null
130
160
  } }),
131
161
  update: (model, message) => SseMessage.match(message, {
132
- Connecting: () => ({ model: {
162
+ Connecting: () => ({ model: unlessSame(model, {
133
163
  ...model,
134
164
  status: "connecting"
135
- } }),
136
- Opened: () => ({ model: {
165
+ }) }),
166
+ Opened: () => ({ model: unlessSame(model, {
137
167
  ...model,
138
168
  status: "open",
139
169
  lastError: null
140
- } }),
170
+ }) }),
141
171
  Received: () => ({ model }),
142
- Closed: () => ({ model: {
172
+ Closed: () => ({ model: unlessSame(model, {
143
173
  ...model,
144
174
  status: "closed"
145
- } }),
146
- Failed: ({ message }) => ({ model: {
175
+ }) }),
176
+ Failed: ({ message }) => ({ model: unlessSame(model, {
147
177
  ...model,
148
178
  status: "connecting",
149
179
  lastError: message
150
- } })
180
+ }) })
151
181
  }),
152
182
  resources: () => ManagedResource.make()((entry) => ({ source: entry(Schema.Option(Schema.String), {
153
183
  resource: Source,
@@ -179,13 +209,9 @@ const sse = (config) => {
179
209
  source.close();
180
210
  }), Queue.shutdown(events)))
181
211
  }) })),
182
- subscriptions: () => Subscription.make()((entry) => ({ incoming: entry({ status: Schema.Literals([
183
- "closed",
184
- "connecting",
185
- "open"
186
- ]) }, {
187
- modelToDependencies: (model) => ({ status: model.status }),
188
- dependenciesToStream: ({ status }) => status === "closed" ? Stream.empty : Stream.unwrap(Effect.matchEffect(Source.get, {
212
+ subscriptions: () => Subscription.make()((entry) => ({ incoming: entry({ live: Schema.Boolean }, {
213
+ modelToDependencies: (model) => ({ live: model.status !== "closed" }),
214
+ dependenciesToStream: ({ live }) => !live ? Stream.empty : Stream.unwrap(Effect.matchEffect(Source.get, {
189
215
  onFailure: () => Effect.succeed(Stream.empty),
190
216
  onSuccess: ({ source, events }) => Effect.succeed(Stream.concat(source.readyState === 1 ? Stream.make(SseMessage.Opened()) : Stream.empty, Stream.fromQueue(events)))
191
217
  }))
@@ -209,7 +235,9 @@ const WebSocketModel = Schema.Struct({
209
235
  "connecting",
210
236
  "open"
211
237
  ]),
212
- lastError: Schema.NullOr(Schema.String)
238
+ lastError: Schema.NullOr(Schema.String),
239
+ /** Whether this connection attempt ever reached open; reset on Connecting. */
240
+ opened: Schema.Boolean
213
241
  });
214
242
  const WebSocketMessage = defineMessageUnion({
215
243
  Connecting: {},
@@ -219,49 +247,101 @@ const WebSocketMessage = defineMessageUnion({
219
247
  Closed: {},
220
248
  Failed: { message: Schema.String },
221
249
  SendFailed: { message: Schema.String },
222
- /** Acknowledges dispatch, not delivery; update ignores it. */
223
- Sent: {}
250
+ /** Acknowledges dispatch of `data`, not its delivery; update ignores it. */
251
+ Sent: { data: Schema.String },
252
+ /** The socket was still connecting when `connectTimeoutMs` ran out, and was closed. */
253
+ TimedOut: {}
254
+ });
255
+ /**
256
+ * What the reader sees: the socket's state, or its error. A page derives
257
+ * this instead of keeping its own connection state machine. `lastError` is
258
+ * already worded for the reader (`Failed to connect to WebSocket`, never the
259
+ * socket's own text, which error handling drops in favor of the sentence).
260
+ */
261
+ const SocketView = defineTaggedUnion({
262
+ Disconnected: {},
263
+ Connecting: {},
264
+ Connected: {},
265
+ Error: { error: Schema.String }
224
266
  });
267
+ /**
268
+ * Whether the socket is usable: open with no error outstanding. Gates sends
269
+ * and received frames.
270
+ */
271
+ const isOpen = (model) => model.status === "open" && model.lastError === null;
272
+ /**
273
+ * The view of a socket, and whether its page still wants it. An error stands
274
+ * until the page stops wanting the socket; a retry still waiting for its
275
+ * socket reads as connecting, so it answers at once. An error on an open
276
+ * socket always reads as an error.
277
+ */
278
+ const viewOf = (model, wanted) => {
279
+ if (model.lastError !== null) return model.status === "open" || !wanted ? SocketView.Error({ error: model.lastError }) : SocketView.Connecting();
280
+ if (model.status === "open") return SocketView.Connected();
281
+ if (model.status === "connecting" || wanted) return SocketView.Connecting();
282
+ return SocketView.Disconnected();
283
+ };
225
284
  /** The live connection, for `send` and the incoming stream. One assembly holds one. */
226
285
  const Socket = ManagedResource.tag()("websocket");
227
286
  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));
287
+ /** Closes the socket with its handlers detached first, so the close reports nothing. */
288
+ const closeSilently = (socket) => {
289
+ socket.onopen = null;
290
+ socket.onmessage = null;
291
+ socket.onclose = null;
292
+ socket.onerror = null;
293
+ socket.close();
294
+ };
228
295
  const failMessage = (error) => error instanceof Error ? error.message : String(error);
229
296
  const websocket = (config) => {
230
297
  const createSocket = config.createSocket ?? ((url) => new WebSocket(url));
231
298
  return Bundle.make(config.name, {
232
299
  Model: WebSocketModel,
233
300
  Message: WebSocketMessage,
234
- args: Schema.Struct({ url: Schema.String }),
301
+ args: Schema.Struct({
302
+ url: Schema.String,
303
+ /** How long a socket may stay connecting before it is closed as `TimedOut`; unbounded when absent. */
304
+ connectTimeoutMs: Schema.optional(Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())))
305
+ }),
235
306
  init: (args) => ({ model: {
236
307
  url: args.url,
237
308
  status: "closed",
238
- lastError: null
309
+ lastError: null,
310
+ opened: false
239
311
  } }),
240
312
  update: (model, message) => WebSocketMessage.match(message, {
241
- Connecting: () => ({ model: {
313
+ Connecting: () => ({ model: unlessSame(model, {
242
314
  ...model,
243
- status: "connecting"
244
- } }),
245
- Opened: () => ({ model: {
315
+ status: "connecting",
316
+ lastError: null,
317
+ opened: false
318
+ }) }),
319
+ Opened: () => ({ model: unlessSame(model, {
246
320
  ...model,
247
321
  status: "open",
248
- lastError: null
249
- } }),
322
+ lastError: null,
323
+ opened: true
324
+ }) }),
250
325
  Received: () => ({ model }),
251
- Closed: () => ({ model: {
326
+ Closed: () => ({ model: unlessSame(model, {
252
327
  ...model,
253
328
  status: "closed"
254
- } }),
255
- Failed: ({ message }) => ({ model: {
329
+ }) }),
330
+ Failed: () => ({ model: unlessSame(model, {
256
331
  ...model,
257
332
  status: "closed",
258
- lastError: message
259
- } }),
260
- SendFailed: ({ message }) => ({ model: {
333
+ lastError: model.status === "open" ? "Connection error" : "Failed to connect to WebSocket"
334
+ }) }),
335
+ SendFailed: () => ({ model: unlessSame(model, {
261
336
  ...model,
262
- lastError: message
263
- } }),
264
- Sent: () => ({ model })
337
+ lastError: "Socket unavailable"
338
+ }) }),
339
+ Sent: () => ({ model }),
340
+ TimedOut: () => ({ model: unlessSame(model, {
341
+ ...model,
342
+ status: "closed",
343
+ lastError: "Connection timeout"
344
+ }) })
265
345
  }),
266
346
  resources: () => ManagedResource.make()((entry) => ({ socket: entry(Schema.Option(Schema.String), {
267
347
  resource: Socket,
@@ -289,25 +369,28 @@ const websocket = (config) => {
289
369
  onAcquired: () => WebSocketMessage.Connecting(),
290
370
  onReleased: () => WebSocketMessage.Closed(),
291
371
  onAcquireError: (error) => WebSocketMessage.Failed({ message: failMessage(error) }),
292
- release: ({ socket, events }) => Effect.asVoid(Effect.andThen(Effect.sync(() => {
293
- socket.onopen = null;
294
- socket.onmessage = null;
295
- socket.onclose = null;
296
- socket.onerror = null;
297
- socket.close();
298
- }), Queue.shutdown(events)))
299
- }) })),
300
- subscriptions: () => Subscription.make()((entry) => ({ incoming: entry({ status: Schema.Literals([
301
- "closed",
302
- "connecting",
303
- "open"
304
- ]) }, {
305
- modelToDependencies: (model) => ({ status: model.status }),
306
- dependenciesToStream: ({ status }) => status === "closed" ? Stream.empty : Stream.unwrap(Effect.matchEffect(Socket.get, {
307
- onFailure: () => Effect.succeed(Stream.empty),
308
- onSuccess: ({ socket, events }) => Effect.succeed(Stream.concat(socket.readyState === 1 ? Stream.make(WebSocketMessage.Opened()) : Stream.empty, Stream.fromQueue(events)))
309
- }))
372
+ release: ({ socket, events }) => Effect.asVoid(Effect.andThen(Effect.sync(() => closeSilently(socket)), Queue.shutdown(events)))
310
373
  }) })),
374
+ subscriptions: ({ connectTimeoutMs }) => Subscription.make()((entry) => ({
375
+ connectTimeout: entry({ connecting: Schema.Boolean }, {
376
+ modelToDependencies: (model) => ({ connecting: model.status === "connecting" }),
377
+ dependenciesToStream: ({ connecting }) => !connecting || connectTimeoutMs === void 0 ? Stream.empty : Stream.unwrap(Effect.matchEffect(Effect.andThen(Effect.sleep(connectTimeoutMs), Socket.get), {
378
+ onFailure: () => Effect.succeed(Stream.empty),
379
+ onSuccess: ({ socket }) => Effect.sync(() => {
380
+ if (socket.readyState !== 0) return Stream.empty;
381
+ closeSilently(socket);
382
+ return Stream.make(WebSocketMessage.TimedOut());
383
+ })
384
+ }))
385
+ }),
386
+ incoming: entry({ live: Schema.Boolean }, {
387
+ modelToDependencies: (model) => ({ live: model.status !== "closed" }),
388
+ dependenciesToStream: ({ live }) => !live ? Stream.empty : Stream.unwrap(Effect.matchEffect(Socket.get, {
389
+ onFailure: () => Effect.succeed(Stream.empty),
390
+ onSuccess: ({ socket, events }) => Effect.succeed(Stream.concat(socket.readyState === 1 ? Stream.make(WebSocketMessage.Opened()) : Stream.empty, Stream.fromQueue(events)))
391
+ }))
392
+ })
393
+ })),
311
394
  helpers: { send: (model, data) => ({
312
395
  model,
313
396
  commands: [{
@@ -315,7 +398,7 @@ const websocket = (config) => {
315
398
  args: { data },
316
399
  effect: Effect.matchEffect(Effect.flatMap(Socket.get, ({ socket }) => socket.readyState === 1 ? Effect.map(Effect.sync(() => {
317
400
  socket.send(data);
318
- }), () => WebSocketMessage.Sent()) : Effect.fail(/* @__PURE__ */ new Error(`cannot send: socket is not open (readyState ${socket.readyState})`))), {
401
+ }), () => WebSocketMessage.Sent({ data })) : Effect.fail(/* @__PURE__ */ new Error(`cannot send: socket is not open (readyState ${socket.readyState})`))), {
319
402
  onFailure: (error) => Effect.succeed(WebSocketMessage.SendFailed({ message: failMessage(error) })),
320
403
  onSuccess: (message) => Effect.succeed(message)
321
404
  })
@@ -324,6 +407,6 @@ const websocket = (config) => {
324
407
  });
325
408
  };
326
409
  //#endregion
327
- export { BroadcastMessage, Online, OnlineMessage, OnlineModel, Socket, Source, SseMessage, SseModel, WebSocketMessage, WebSocketModel, broadcastMessages, postBroadcast, sse, websocket };
410
+ export { BroadcastMessage, Online, OnlineMessage, OnlineModel, Socket, SocketView, Source, SseMessage, SseModel, SseView, WebSocketMessage, WebSocketModel, broadcastMessages, isLive, isOpen, postBroadcast, sse, viewOf, viewOfSse, websocket };
328
411
 
329
412
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["failMessage","failMessage"],"sources":["../../src/net/broadcast.ts","../../src/net/online.ts","../../src/net/sse.ts","../../src/net/websocket.ts"],"sourcesContent":["/**\n * Cross-tab messaging as an entry plus a Command — not a bundle, because the\n * channel owns no state: the parent maps `Received` into its own Message and\n * stores whatever it keeps. One assembly may open the same name twice (two\n * listeners); share the entry instead. By spec a post never echoes to its\n * own channel, so a tab never hears itself.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const BroadcastMessage = defineMessageUnion({\n /** Notifies without storing: project the payload into your own field to keep it. */\n Received: { data: Schema.Unknown },\n Posted: {},\n BroadcastFailed: { message: Schema.String },\n})\nexport type BroadcastMessage = typeof BroadcastMessage.Type\n\n/** The slice of a BroadcastChannel this module needs; satisfied by the platform class. */\nexport interface BroadcastChannelHandle {\n postMessage(data: unknown): void\n close(): void\n onmessage: ((event: { readonly data: unknown }) => void) | null\n}\n\ntype ChannelFactory = (name: string) => BroadcastChannelHandle\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\nconst createChannel = (\n factory: ChannelFactory | undefined,\n name: string,\n): BroadcastChannelHandle | null => {\n if (factory !== undefined) return factory(name)\n if (typeof BroadcastChannel === 'undefined') return null\n return new BroadcastChannel(name) as unknown as BroadcastChannelHandle\n}\n\n/**\n * Every post to `name` from another channel, as Messages. Without a\n * BroadcastChannel API the stream is empty instead of throwing; closing the\n * stream closes the channel.\n */\nexport const broadcastMessages = (\n name: string,\n /** Read lazily so tests can substitute a double, like the SSE bundle. */\n create?: ChannelFactory | undefined,\n): Stream.Stream<BroadcastMessage> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const channel = createChannel(create, name)\n if (channel === null) return Stream.empty\n const queue = yield* Queue.unbounded<BroadcastMessage>()\n channel.onmessage = event => {\n Effect.runFork(Queue.offer(queue, BroadcastMessage.Received({ data: event.data })))\n }\n yield* Effect.addFinalizer(() =>\n Effect.sync(() => {\n channel.onmessage = null\n channel.close()\n }),\n )\n return Stream.fromQueue(queue)\n }),\n )\n\n/**\n * Posts one message to `name`. Opens a channel, posts, and closes it: the\n * send is one-shot, so it is a Command, not a resource. Without a\n * BroadcastChannel API — or when the post throws — it yields\n * `BroadcastFailed` instead of throwing.\n */\nexport const postBroadcast = (\n name: string,\n data: unknown,\n create?: ChannelFactory | undefined,\n): Command<BroadcastMessage, never, never> => ({\n name: 'Broadcast.post',\n args: { name, data },\n // One path for every failure: no factory and no platform API, or a post\n // that throws (a closed channel) all become BroadcastFailed.\n effect: Effect.matchEffect(\n Effect.try({\n try: () => {\n const channel = createChannel(create, name)\n if (channel === null) throw new Error('broadcast is unavailable')\n try {\n channel.postMessage(data)\n } finally {\n channel.close()\n }\n },\n catch: failMessage,\n }),\n {\n onFailure: message => Effect.succeed(BroadcastMessage.BroadcastFailed({ message })),\n onSuccess: () => Effect.succeed(BroadcastMessage.Posted()),\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 */\nimport { 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\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) => ({ model: { online: message.online } }),\n subscriptions: (): Subscription.Subscriptions<OnlineModel, OnlineMessage> =>\n Subscription.make<OnlineModel, OnlineMessage>()(() => ({\n changes: Subscription.persistent(presenceStream()),\n })),\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 */\nimport { Effect, Option, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as ManagedResource from 'foldkit/managedResource'\nimport * as Subscription from 'foldkit/subscription'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\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.NullOr(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/** 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\ntype SourceResources = Readonly<{\n source: ManagedResource.Entry<\n SseModel,\n SseMessage,\n Option.Option<string>,\n AcquiredSource,\n SourceService,\n () => SseMessage\n >\n}>\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(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<\n Name,\n SseModel,\n SseMessage,\n { readonly url: string },\n never,\n SourceService,\n SourceService,\n void,\n SourceResources,\n {}\n >(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: null },\n }),\n update: (\n model,\n message,\n ): Update.ReturnWithOutMessage<SseModel, SseMessage, never, SourceService> =>\n SseMessage.match(message, {\n Connecting: () => ({ model: { ...model, status: 'connecting' as const } }),\n Opened: () => ({ model: { ...model, status: 'open' as const, lastError: null } }),\n Received: () => ({ model }),\n Closed: () => ({ model: { ...model, status: 'closed' as const } }),\n Failed: ({ message }) => ({\n model: { ...model, status: 'connecting' as const, lastError: message },\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 { status: Schema.Literals(['closed', 'connecting', 'open']) },\n {\n modelToDependencies: model => ({ status: model.status }),\n dependenciesToStream: ({ status }) =>\n status === 'closed'\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","/**\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 */\nimport { Effect, Option, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as ManagedResource from 'foldkit/managedResource'\nimport * as Subscription from 'foldkit/subscription'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\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.NullOr(Schema.String),\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, not delivery; update ignores it. */\n Sent: {},\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/** 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\ntype SocketResources = Readonly<{\n socket: ManagedResource.Entry<\n WebSocketModel,\n WebSocketMessage,\n Option.Option<string>,\n Acquired,\n SocketService,\n () => WebSocketMessage\n >\n}>\n\ntype SendHelpers = Readonly<{\n send: (\n model: WebSocketModel,\n data: string,\n ) => Update.ReturnWithOutMessage<WebSocketModel, WebSocketMessage, never, SocketService>\n}>\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\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\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<\n Name,\n WebSocketModel,\n WebSocketMessage,\n { readonly url: string },\n never,\n SocketService,\n SocketService,\n void,\n SocketResources,\n SendHelpers\n >(config.name, {\n Model: WebSocketModel,\n Message: WebSocketMessage,\n args: Schema.Struct({ url: Schema.String }),\n init: (args): Update.Return<WebSocketModel, WebSocketMessage, SocketService> => ({\n model: { url: args.url, status: 'closed', lastError: null },\n }),\n update: (\n model,\n message,\n ): Update.ReturnWithOutMessage<WebSocketModel, WebSocketMessage, never, SocketService> =>\n WebSocketMessage.match(message, {\n Connecting: () => ({ model: { ...model, status: 'connecting' as const } }),\n Opened: () => ({ model: { ...model, status: 'open' as const, lastError: null } }),\n Received: () => ({ model }),\n Closed: () => ({ model: { ...model, status: 'closed' as const } }),\n Failed: ({ message }) => ({\n model: { ...model, status: 'closed' as const, lastError: message },\n }),\n SendFailed: ({ message }) => ({ model: { ...model, lastError: message } }),\n Sent: () => ({ model }),\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(() => {\n socket.onopen = null\n socket.onmessage = null\n socket.onclose = null\n socket.onerror = null\n socket.close()\n }),\n Queue.shutdown(events),\n ),\n ),\n }),\n })),\n subscriptions: (): Subscription.Subscriptions<\n WebSocketModel,\n WebSocketMessage,\n SocketService\n > =>\n Subscription.make<WebSocketModel, WebSocketMessage, SocketService>()(entry => ({\n incoming: entry(\n { status: Schema.Literals(['closed', 'connecting', 'open']) },\n {\n modelToDependencies: model => ({ status: model.status }),\n dependenciesToStream: ({ status }) =>\n status === 'closed'\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, data) => ({\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(),\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":";;;;;;;;;;;;;AAWA,MAAa,mBAAmB,mBAAmB;;CAEjD,UAAU,EAAE,MAAM,OAAO,QAAQ;CACjC,QAAQ,CAAC;CACT,iBAAiB,EAAE,SAAS,OAAO,OAAO;AAC5C,CAAC;AAYD,MAAMA,iBAAe,UACnB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAEvD,MAAM,iBACJ,SACA,SACkC;CAClC,IAAI,YAAY,KAAA,GAAW,OAAO,QAAQ,IAAI;CAC9C,IAAI,OAAO,qBAAqB,aAAa,OAAO;CACpD,OAAO,IAAI,iBAAiB,IAAI;AAClC;;;;;;AAOA,MAAa,qBACX,MAEA,WAEA,OAAO,OACL,OAAO,IAAI,aAAa;CACtB,MAAM,UAAU,cAAc,QAAQ,IAAI;CAC1C,IAAI,YAAY,MAAM,OAAO,OAAO;CACpC,MAAM,QAAQ,OAAO,MAAM,UAA4B;CACvD,QAAQ,aAAY,UAAS;EAC3B,OAAO,QAAQ,MAAM,MAAM,OAAO,iBAAiB,SAAS,EAAE,MAAM,MAAM,KAAK,CAAC,CAAC,CAAC;CACpF;CACA,OAAO,OAAO,mBACZ,OAAO,WAAW;EAChB,QAAQ,YAAY;EACpB,QAAQ,MAAM;CAChB,CAAC,CACH;CACA,OAAO,OAAO,UAAU,KAAK;AAC/B,CAAC,CACH;;;;;;;AAQF,MAAa,iBACX,MACA,MACA,YAC6C;CAC7C,MAAM;CACN,MAAM;EAAE;EAAM;CAAK;CAGnB,QAAQ,OAAO,YACb,OAAO,IAAI;EACT,WAAW;GACT,MAAM,UAAU,cAAc,QAAQ,IAAI;GAC1C,IAAI,YAAY,MAAM,MAAM,IAAI,MAAM,0BAA0B;GAChE,IAAI;IACF,QAAQ,YAAY,IAAI;GAC1B,UAAU;IACR,QAAQ,MAAM;GAChB;EACF;EACA,OAAOA;CACT,CAAC,GACD;EACE,YAAW,YAAW,OAAO,QAAQ,iBAAiB,gBAAgB,EAAE,QAAQ,CAAC,CAAC;EAClF,iBAAiB,OAAO,QAAQ,iBAAiB,OAAO,CAAC;CAC3D,CACF;AACF;;;;;;;;AC3FA,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;AAEA,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,aAAa,EAAE,OAAO,EAAE,QAAQ,QAAQ,OAAO,EAAE;CACjE,qBACE,aAAa,KAAiC,CAAC,QAAQ,EACrD,SAAS,aAAa,WAAW,eAAe,CAAC,EACnD,EAAE;AACN,CAAC;;;;;;;;;AC3BD,MAAa,WAAW,OAAO,OAAO;CACpC,KAAK,OAAO;CACZ,QAAQ,OAAO,SAAS;EAAC;EAAU;EAAc;CAAM,CAAC;CACxD,WAAW,OAAO,OAAO,OAAO,MAAM;AACxC,CAAC;AAGD,MAAa,aAAa,mBAAmB;CAC3C,YAAY,CAAC;CACb,QAAQ,CAAC;;CAET,UAAU,EAAE,MAAM,OAAO,OAAO;CAChC,QAAQ,CAAC;CACT,QAAQ,EAAE,SAAS,OAAO,OAAO;AACnC,CAAC;;AAUD,MAAa,SAAS,gBAAgB,IAAoB,CAAC,CAAC,KAAK;AAgBjE,MAAMC,iBAAe,UACnB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAEvD,MAAa,OAAkC,WAIzC;CACJ,MAAM,eACJ,OAAO,kBAAiB,QAAO,IAAI,YAAY,GAAG;CACpD,OAAO,OAAO,KAWZ,OAAO,MAAM;EACb,OAAO;EACP,SAAS;EACT,MAAM,OAAO,OAAO,EAAE,KAAK,OAAO,OAAO,CAAC;EAC1C,OAAO,UAA8D,EACnE,OAAO;GAAE,KAAK,KAAK;GAAK,QAAQ;GAAU,WAAW;EAAK,EAC5D;EACA,SACE,OACA,YAEA,WAAW,MAAM,SAAS;GACxB,mBAAmB,EAAE,OAAO;IAAE,GAAG;IAAO,QAAQ;GAAsB,EAAE;GACxE,eAAe,EAAE,OAAO;IAAE,GAAG;IAAO,QAAQ;IAAiB,WAAW;GAAK,EAAE;GAC/E,iBAAiB,EAAE,MAAM;GACzB,eAAe,EAAE,OAAO;IAAE,GAAG;IAAO,QAAQ;GAAkB,EAAE;GAChE,SAAS,EAAE,eAAe,EACxB,OAAO;IAAE,GAAG;IAAO,QAAQ;IAAuB,WAAW;GAAQ,EACvE;EACF,CAAC;EACH,iBACE,gBAAgB,KAA2B,CAAC,EAAC,WAAU,EACrD,QAAQ,MAAM,OAAO,OAAO,OAAO,MAAM,GAAG;GAC1C,UAAU;GACV,2BAA0B,UAAS,OAAO,KAAK,MAAM,GAAG;GACxD,UAAU,QACR,OAAO,IAAI,aAAa;IACtB,MAAM,SAAS,OAAO,MAAM,UAAsB;IAClD,MAAM,SAAS,aAAa,GAAG;IAE/B,IAAI,OAAsB,QAAQ,QAAQ;IAC1C,MAAM,WAAW,WAAyC;KACxD,MAAM,YACJ,OAAO,WAAW,MAAM,CAAC,CAAC,WAClB,KAAA,SACA,KAAA,CACR;KACF,OAAO,KAAK,KAAK,KAAK,GAAG;IAC3B;IACA,MAAM,SAAS,YAAwB,QAAQ,MAAM,MAAM,QAAQ,OAAO,CAAC;IAC3E,OAAO,eAAe,MAAM,WAAW,OAAO,CAAC;IAE/C,OAAO,aAAY,UAAS,MAAM,WAAW,SAAS,EAAE,MAAM,OAAO,MAAM,IAAI,EAAE,CAAC,CAAC;IACnF,OAAO,gBACL,MAAM,WAAW,OAAO,EAAE,SAAS,0BAA0B,MAAM,CAAC,CAAC;IACvE,OAAO;KAAE;KAAQ;IAAO;GAC1B,CAAC;GACH,kBAAkB,WAAW,WAAW;GACxC,kBAAkB,WAAW,OAAO;GACpC,iBAAgB,UAAS,WAAW,OAAO,EAAE,SAASA,cAAY,KAAK,EAAE,CAAC;GAE1E,UAAU,EAAE,QAAQ,aAClB,OAAO,OACL,OAAO,QACL,OAAO,WAAW;IAChB,OAAO,SAAS;IAChB,OAAO,YAAY;IACnB,OAAO,UAAU;IACjB,OAAO,MAAM;GACf,CAAC,GACD,MAAM,SAAS,MAAM,CACvB,CACF;EACJ,CAAC,EACH,EAAE;EACJ,qBACE,aAAa,KAA0C,CAAC,EAAC,WAAU,EACjE,UAAU,MACR,EAAE,QAAQ,OAAO,SAAS;GAAC;GAAU;GAAc;EAAM,CAAC,EAAE,GAC5D;GACE,sBAAqB,WAAU,EAAE,QAAQ,MAAM,OAAO;GACtD,uBAAuB,EAAE,aACvB,WAAW,WACP,OAAO,QACP,OAAO,OACL,OAAO,YAAY,OAAO,KAAK;IAG7B,iBAAiB,OAAO,QAAQ,OAAO,KAAK;IAC5C,YAAY,EAAE,QAAQ,aACpB,OAAO,QACL,OAAO,OAGL,OAAO,eAAe,IAClB,OAAO,KAAK,WAAW,OAAO,CAAC,IAC/B,OAAO,OACX,OAAO,UAAU,MAAM,CACzB,CACF;GACJ,CAAC,CACH;EACR,CACF,EACF,EAAE;CACN,CAAC;AACH;;;;;;;;;;;ACzJA,MAAa,iBAAiB,OAAO,OAAO;CAC1C,KAAK,OAAO;CACZ,QAAQ,OAAO,SAAS;EAAC;EAAU;EAAc;CAAM,CAAC;CACxD,WAAW,OAAO,OAAO,OAAO,MAAM;AACxC,CAAC;AAGD,MAAa,mBAAmB,mBAAmB;CACjD,YAAY,CAAC;CACb,QAAQ,CAAC;;CAET,UAAU,EAAE,MAAM,OAAO,OAAO;CAChC,QAAQ,CAAC;CACT,QAAQ,EAAE,SAAS,OAAO,OAAO;CACjC,YAAY,EAAE,SAAS,OAAO,OAAO;;CAErC,MAAM,CAAC;AACT,CAAC;;AAUD,MAAa,SAAS,gBAAgB,IAAc,CAAC,CAAC,WAAW;AAuBjE,MAAM,UAAU,SACd,OAAO,SAAS,WACZ,OAAO,QAAQ,IAAI,IACnB,gBAAgB,OACd,OAAO,cAAc,KAAK,KAAK,CAAC,IAChC,gBAAgB,eAAe,YAAY,OAAO,IAAI,IACpD,OAAO,QAAQ,IAAI,YAAY,CAAC,CAAC,OAAO,IAAI,CAAC,IAC7C,OAAO,QAAQ,OAAO,IAAI,CAAC;AAErC,MAAM,eAAe,UACnB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAEvD,MAAa,aAAwC,WAI/C;CACJ,MAAM,eACJ,OAAO,kBAAiB,QAAO,IAAI,UAAU,GAAG;CAClD,OAAO,OAAO,KAWZ,OAAO,MAAM;EACb,OAAO;EACP,SAAS;EACT,MAAM,OAAO,OAAO,EAAE,KAAK,OAAO,OAAO,CAAC;EAC1C,OAAO,UAA0E,EAC/E,OAAO;GAAE,KAAK,KAAK;GAAK,QAAQ;GAAU,WAAW;EAAK,EAC5D;EACA,SACE,OACA,YAEA,iBAAiB,MAAM,SAAS;GAC9B,mBAAmB,EAAE,OAAO;IAAE,GAAG;IAAO,QAAQ;GAAsB,EAAE;GACxE,eAAe,EAAE,OAAO;IAAE,GAAG;IAAO,QAAQ;IAAiB,WAAW;GAAK,EAAE;GAC/E,iBAAiB,EAAE,MAAM;GACzB,eAAe,EAAE,OAAO;IAAE,GAAG;IAAO,QAAQ;GAAkB,EAAE;GAChE,SAAS,EAAE,eAAe,EACxB,OAAO;IAAE,GAAG;IAAO,QAAQ;IAAmB,WAAW;GAAQ,EACnE;GACA,aAAa,EAAE,eAAe,EAAE,OAAO;IAAE,GAAG;IAAO,WAAW;GAAQ,EAAE;GACxE,aAAa,EAAE,MAAM;EACvB,CAAC;EACH,iBACE,gBAAgB,KAAuC,CAAC,EAAC,WAAU,EACjE,QAAQ,MAAM,OAAO,OAAO,OAAO,MAAM,GAAG;GAC1C,UAAU;GACV,2BAA0B,UAAS,OAAO,KAAK,MAAM,GAAG;GACxD,UAAU,QACR,OAAO,IAAI,aAAa;IACtB,MAAM,SAAS,OAAO,MAAM,UAA4B;IACxD,MAAM,SAAS,aAAa,GAAG;IAI/B,IAAI,OAAsB,QAAQ,QAAQ;IAC1C,MAAM,WAAW,WAAyC;KACxD,MAAM,YACJ,OAAO,WAAW,MAAM,CAAC,CAAC,WAClB,KAAA,SACA,KAAA,CACR;KACF,OAAO,KAAK,KAAK,KAAK,GAAG;IAC3B;IACA,MAAM,SAAS,YAA8B,QAAQ,MAAM,MAAM,QAAQ,OAAO,CAAC;IACjF,OAAO,eAAe,MAAM,iBAAiB,OAAO,CAAC;IACrD,OAAO,aAAY,UAAS;KAC1B,QACE,OAAO,QAAQ,OAAO,MAAM,IAAI,IAAG,SACjC,MAAM,MAAM,QAAQ,iBAAiB,SAAS,EAAE,MAAM,KAAK,CAAC,CAAC,CAC/D,CACF;IACF;IACA,OAAO,gBAAgB,MAAM,iBAAiB,OAAO,CAAC;IACtD,OAAO,gBACL,MAAM,iBAAiB,OAAO,EAAE,SAAS,uBAAuB,MAAM,CAAC,CAAC;IAC1E,OAAO;KAAE;KAAQ;IAAO;GAC1B,CAAC;GACH,kBAAkB,iBAAiB,WAAW;GAC9C,kBAAkB,iBAAiB,OAAO;GAC1C,iBAAgB,UAAS,iBAAiB,OAAO,EAAE,SAAS,YAAY,KAAK,EAAE,CAAC;GAEhF,UAAU,EAAE,QAAQ,aAClB,OAAO,OACL,OAAO,QACL,OAAO,WAAW;IAChB,OAAO,SAAS;IAChB,OAAO,YAAY;IACnB,OAAO,UAAU;IACjB,OAAO,UAAU;IACjB,OAAO,MAAM;GACf,CAAC,GACD,MAAM,SAAS,MAAM,CACvB,CACF;EACJ,CAAC,EACH,EAAE;EACJ,qBAKE,aAAa,KAAsD,CAAC,EAAC,WAAU,EAC7E,UAAU,MACR,EAAE,QAAQ,OAAO,SAAS;GAAC;GAAU;GAAc;EAAM,CAAC,EAAE,GAC5D;GACE,sBAAqB,WAAU,EAAE,QAAQ,MAAM,OAAO;GACtD,uBAAuB,EAAE,aACvB,WAAW,WACP,OAAO,QACP,OAAO,OACL,OAAO,YAAY,OAAO,KAAK;IAG7B,iBAAiB,OAAO,QAAQ,OAAO,KAAK;IAC5C,YAAY,EAAE,QAAQ,aACpB,OAAO,QACL,OAAO,OAIL,OAAO,eAAe,IAClB,OAAO,KAAK,iBAAiB,OAAO,CAAC,IACrC,OAAO,OACX,OAAO,UAAU,MAAM,CACzB,CACF;GACJ,CAAC,CACH;EACR,CACF,EACF,EAAE;EACJ,SAAS,EACP,OAAO,OAAO,UAAU;GACtB;GACA,UAAU,CACR;IACE,MAAM;IACN,MAAM,EAAE,KAAK;IACb,QAAQ,OAAO,YACb,OAAO,QAAQ,OAAO,MAAM,EAAE,aAG5B,OAAO,eAAe,IAClB,OAAO,IACL,OAAO,WAAW;KAChB,OAAO,KAAK,IAAI;IAClB,CAAC,SACK,iBAAiB,KAAK,CAC9B,IACA,OAAO,qBACL,IAAI,MACF,+CAA+C,OAAO,WAAW,EACnE,CACF,CACN,GACA;KACE,YAAW,UACT,OAAO,QAAQ,iBAAiB,WAAW,EAAE,SAAS,YAAY,KAAK,EAAE,CAAC,CAAC;KAC7E,YAAW,YAAW,OAAO,QAAQ,OAAO;IAC9C,CACF;GACF,CACF;EACF,GACF;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"index.mjs","names":["failMessage","failMessage"],"sources":["../../src/net/broadcast.ts","../../src/net/online.ts","../../src/net/sse.ts","../../src/net/websocket.ts"],"sourcesContent":["/**\n * Cross-tab messaging as an entry plus a Command — not a bundle, because the\n * channel owns no state: the parent maps `Received` into its own Message and\n * stores whatever it keeps. One assembly may open the same name twice (two\n * listeners); share the entry instead. By spec a post never echoes to its\n * own channel, so a tab never hears itself.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\n\nexport const BroadcastMessage = defineMessageUnion({\n /** Notifies without storing: project the payload into your own field to keep it. */\n Received: { data: Schema.Unknown },\n Posted: {},\n BroadcastFailed: { message: Schema.String },\n})\nexport type BroadcastMessage = typeof BroadcastMessage.Type\n\n/** The slice of a BroadcastChannel this module needs; satisfied by the platform class. */\nexport interface BroadcastChannelHandle {\n postMessage(data: unknown): void\n close(): void\n onmessage: ((event: { readonly data: unknown }) => void) | null\n}\n\ntype ChannelFactory = (name: string) => BroadcastChannelHandle\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\nconst createChannel = (\n factory: ChannelFactory | undefined,\n name: string,\n): BroadcastChannelHandle | null => {\n if (factory !== undefined) return factory(name)\n if (typeof BroadcastChannel === 'undefined') return null\n return new BroadcastChannel(name) as unknown as BroadcastChannelHandle\n}\n\n/**\n * Every post to `name` from another channel, as Messages. Without a\n * BroadcastChannel API the stream is empty instead of throwing; closing the\n * stream closes the channel.\n */\nexport const broadcastMessages = (\n name: string,\n /** Read lazily so tests can substitute a double, like the SSE bundle. */\n create?: ChannelFactory | undefined,\n): Stream.Stream<BroadcastMessage> =>\n Stream.unwrap(\n Effect.gen(function* () {\n const channel = createChannel(create, name)\n if (channel === null) return Stream.empty\n const queue = yield* Queue.unbounded<BroadcastMessage>()\n channel.onmessage = event => {\n Effect.runFork(Queue.offer(queue, BroadcastMessage.Received({ data: event.data })))\n }\n yield* Effect.addFinalizer(() =>\n Effect.sync(() => {\n channel.onmessage = null\n channel.close()\n }),\n )\n return Stream.fromQueue(queue)\n }),\n )\n\n/**\n * Posts one message to `name`. Opens a channel, posts, and closes it: the\n * send is one-shot, so it is a Command, not a resource. Without a\n * BroadcastChannel API — or when the post throws — it yields\n * `BroadcastFailed` instead of throwing.\n */\nexport const postBroadcast = (\n name: string,\n data: unknown,\n create?: ChannelFactory | undefined,\n): Command<BroadcastMessage, never, never> => ({\n name: 'Broadcast.post',\n args: { name, data },\n // One path for every failure: no factory and no platform API, or a post\n // that throws (a closed channel) all become BroadcastFailed.\n effect: Effect.matchEffect(\n Effect.try({\n try: () => {\n const channel = createChannel(create, name)\n if (channel === null) throw new Error('broadcast is unavailable')\n try {\n channel.postMessage(data)\n } finally {\n channel.close()\n }\n },\n catch: failMessage,\n }),\n {\n onFailure: message => Effect.succeed(BroadcastMessage.BroadcastFailed({ message })),\n onSuccess: () => Effect.succeed(BroadcastMessage.Posted()),\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 */\nimport { 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\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.persistent(presenceStream()),\n })),\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 */\nimport { 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.NullOr(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' && model.lastError === null\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 if (model.lastError !== null)\n return model.status === 'open' || !wanted\n ? SseView.Error({ error: model.lastError })\n : SseView.Connecting()\n if (model.status === 'open') return SseView.Connected()\n if (model.status === 'connecting' || wanted) return SseView.Connecting()\n return SseView.Disconnected()\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\ntype SourceResources = Readonly<{\n source: ManagedResource.Entry<\n SseModel,\n SseMessage,\n Option.Option<string>,\n AcquiredSource,\n SourceService,\n () => SseMessage\n >\n}>\n\nconst failMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(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<\n Name,\n SseModel,\n SseMessage,\n { readonly url: string },\n never,\n SourceService,\n SourceService,\n void,\n SourceResources,\n {}\n >(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: null },\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: null }),\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, { ...model, status: 'connecting', lastError: message }),\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","/**\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 */\nimport { 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.NullOr(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' && model.lastError === null\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 if (model.lastError !== null)\n return model.status === 'open' || !wanted\n ? SocketView.Error({ error: model.lastError })\n : SocketView.Connecting()\n if (model.status === 'open') return SocketView.Connected()\n if (model.status === 'connecting' || wanted) return SocketView.Connecting()\n return SocketView.Disconnected()\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\ntype SocketResources = Readonly<{\n socket: ManagedResource.Entry<\n WebSocketModel,\n WebSocketMessage,\n Option.Option<string>,\n Acquired,\n SocketService,\n () => WebSocketMessage\n >\n}>\n\ntype SendHelpers = Readonly<{\n send: (\n model: WebSocketModel,\n data: string,\n ) => Update.ReturnWithOutMessage<WebSocketModel, WebSocketMessage, never, SocketService>\n}>\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\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<\n Name,\n WebSocketModel,\n WebSocketMessage,\n { readonly url: string; readonly connectTimeoutMs?: number | undefined },\n never,\n SocketService,\n SocketService,\n void,\n SocketResources,\n SendHelpers\n >(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: null, 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: null,\n opened: false,\n }),\n }),\n Opened: () => ({\n model: unlessSame(model, { ...model, status: 'open', lastError: null, opened: true }),\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:\n model.status === 'open' ? 'Connection error' : 'Failed to connect to WebSocket',\n }),\n }),\n SendFailed: () => ({\n model: unlessSame(model, { ...model, lastError: 'Socket unavailable' }),\n }),\n Sent: () => ({ model }),\n TimedOut: () => ({\n model: unlessSame(model, {\n ...model,\n status: 'closed',\n lastError: '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, data) => ({\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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"}
@@ -38,7 +38,7 @@ export declare const Mutated: Schema.TaggedStruct<"Mutated", {
38
38
  export type Mutated = typeof Mutated.Type;
39
39
  export declare const Mutation: Mount.MountDefinitionNoArgs<"Mutation", {
40
40
  readonly _tag: "Mutated";
41
- readonly type: "childList" | "attributes" | "characterData";
41
+ readonly type: "attributes" | "characterData" | "childList";
42
42
  readonly added: readonly string[];
43
43
  readonly removed: readonly string[];
44
44
  readonly attribute: string | null;
@@ -18,8 +18,11 @@ const Bounds = Mount.defineStream("Bounds", {
18
18
  messages: [Measured],
19
19
  execute: ({ element }) => Stream.callback((queue) => Effect.gen(function* () {
20
20
  if (typeof window === "undefined") return;
21
+ let last = null;
21
22
  const measure = () => {
22
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;
23
26
  Queue.offerUnsafe(queue, Measured.make({
24
27
  x: rect.x,
25
28
  y: rect.y,
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/observers/bounds.ts","../../src/observers/intersection.ts","../../src/observers/mutation.ts","../../src/observers/resize.ts"],"sourcesContent":["/**\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 */\nimport { 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\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 const measure = () => {\n const rect = element.getBoundingClientRect()\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","/**\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 */\nimport { 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\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","/**\n * DOM mutations as a Mount: MutationObserver reports child, attribute, and\n * text changes as Messages for the element carrying this Mount. Nodes cross\n * as their names (serializable); the live nodes stay in the DOM, never in\n * the Model. No observer (SSR, old browser) emits nothing instead of\n * throwing. Observes the whole subtree — narrow by placing on a smaller\n * element, not by options.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nexport const Mutated = Schema.TaggedStruct('Mutated', {\n type: Schema.Literals(['childList', 'attributes', 'characterData']),\n added: Schema.Array(Schema.String),\n removed: Schema.Array(Schema.String),\n attribute: Schema.NullOr(Schema.String),\n})\nexport type Mutated = typeof Mutated.Type\n\ntype ObserverCtor = new (callback: MutationCallback) => MutationObserver\n\nconst recordType = (type: string): Mutated['type'] | null =>\n type === 'childList' || type === 'attributes' || type === 'characterData' ? type : null\n\nexport const Mutation = Mount.defineStream('Mutation', {\n messages: [Mutated],\n execute: ({ element }) =>\n Stream.callback<typeof Mutated.Type>(queue =>\n Effect.gen(function* () {\n const Observed = (globalThis as { MutationObserver?: ObserverCtor }).MutationObserver\n if (Observed === undefined) return\n yield* Effect.acquireRelease(\n Effect.sync(() => {\n const observer = new Observed(records => {\n for (const record of records) {\n const type = recordType(record.type)\n if (type === null) continue\n Queue.offerUnsafe(\n queue,\n Mutated.make({\n type,\n added:\n type === 'childList'\n ? Array.from(record.addedNodes, node => node.nodeName)\n : [],\n removed:\n type === 'childList'\n ? Array.from(record.removedNodes, node => node.nodeName)\n : [],\n attribute: type === 'attributes' ? (record.attributeName ?? null) : null,\n }),\n )\n }\n })\n observer.observe(element, {\n childList: true,\n attributes: true,\n characterData: true,\n subtree: true,\n })\n return observer\n }),\n observer => Effect.sync(() => observer.disconnect()),\n )\n }),\n ),\n})\n","/**\n * Element size as a Mount: ResizeObserver reports content-box changes as\n * Messages for the element carrying this Mount. No observer (SSR, old\n * browser) emits nothing instead of throwing.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nexport const Resized = Schema.TaggedStruct('Resized', {\n width: Schema.Number,\n height: Schema.Number,\n})\nexport type Resized = typeof Resized.Type\n\ntype ObserverCtor = new (callback: ResizeObserverCallback) => ResizeObserver\n\nexport const Resize = Mount.defineStream('Resize', {\n messages: [Resized],\n execute: ({ element }) =>\n Stream.callback<typeof Resized.Type>(queue =>\n Effect.gen(function* () {\n const Observed = (globalThis as { ResizeObserver?: ObserverCtor }).ResizeObserver\n if (Observed === undefined) return\n yield* Effect.acquireRelease(\n Effect.sync(() => {\n const observer = new Observed(entries => {\n const rect = entries[0]?.contentRect\n if (rect !== undefined) {\n Queue.offerUnsafe(queue, Resized.make({ width: rect.width, height: rect.height }))\n }\n })\n observer.observe(element)\n return observer\n }),\n observer => Effect.sync(() => observer.disconnect()),\n )\n }),\n ),\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/observers/bounds.ts","../../src/observers/intersection.ts","../../src/observers/mutation.ts","../../src/observers/resize.ts"],"sourcesContent":["/**\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 */\nimport { 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\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","/**\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 */\nimport { 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\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","/**\n * DOM mutations as a Mount: MutationObserver reports child, attribute, and\n * text changes as Messages for the element carrying this Mount. Nodes cross\n * as their names (serializable); the live nodes stay in the DOM, never in\n * the Model. No observer (SSR, old browser) emits nothing instead of\n * throwing. Observes the whole subtree — narrow by placing on a smaller\n * element, not by options.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nexport const Mutated = Schema.TaggedStruct('Mutated', {\n type: Schema.Literals(['childList', 'attributes', 'characterData']),\n added: Schema.Array(Schema.String),\n removed: Schema.Array(Schema.String),\n attribute: Schema.NullOr(Schema.String),\n})\nexport type Mutated = typeof Mutated.Type\n\ntype ObserverCtor = new (callback: MutationCallback) => MutationObserver\n\nconst recordType = (type: string): Mutated['type'] | null =>\n type === 'childList' || type === 'attributes' || type === 'characterData' ? type : null\n\nexport const Mutation = Mount.defineStream('Mutation', {\n messages: [Mutated],\n execute: ({ element }) =>\n Stream.callback<typeof Mutated.Type>(queue =>\n Effect.gen(function* () {\n const Observed = (globalThis as { MutationObserver?: ObserverCtor }).MutationObserver\n if (Observed === undefined) return\n yield* Effect.acquireRelease(\n Effect.sync(() => {\n const observer = new Observed(records => {\n for (const record of records) {\n const type = recordType(record.type)\n if (type === null) continue\n Queue.offerUnsafe(\n queue,\n Mutated.make({\n type,\n added:\n type === 'childList'\n ? Array.from(record.addedNodes, node => node.nodeName)\n : [],\n removed:\n type === 'childList'\n ? Array.from(record.removedNodes, node => node.nodeName)\n : [],\n attribute: type === 'attributes' ? (record.attributeName ?? null) : null,\n }),\n )\n }\n })\n observer.observe(element, {\n childList: true,\n attributes: true,\n characterData: true,\n subtree: true,\n })\n return observer\n }),\n observer => Effect.sync(() => observer.disconnect()),\n )\n }),\n ),\n})\n","/**\n * Element size as a Mount: ResizeObserver reports content-box changes as\n * Messages for the element carrying this Mount. No observer (SSR, old\n * browser) emits nothing instead of throwing.\n */\nimport { Effect, Queue, Schema, Stream } from 'effect'\nimport * as Mount from 'foldkit/mount'\n\nexport const Resized = Schema.TaggedStruct('Resized', {\n width: Schema.Number,\n height: Schema.Number,\n})\nexport type Resized = typeof Resized.Type\n\ntype ObserverCtor = new (callback: ResizeObserverCallback) => ResizeObserver\n\nexport const Resize = Mount.defineStream('Resize', {\n messages: [Resized],\n execute: ({ element }) =>\n Stream.callback<typeof Resized.Type>(queue =>\n Effect.gen(function* () {\n const Observed = (globalThis as { ResizeObserver?: ObserverCtor }).ResizeObserver\n if (Observed === undefined) return\n yield* Effect.acquireRelease(\n Effect.sync(() => {\n const observer = new Observed(entries => {\n const rect = entries[0]?.contentRect\n if (rect !== undefined) {\n Queue.offerUnsafe(queue, Resized.make({ width: rect.width, height: rect.height }))\n }\n })\n observer.observe(element)\n return observer\n }),\n observer => Effect.sync(() => observer.disconnect()),\n )\n }),\n ),\n})\n"],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };