@livestore/utils 0.3.0-dev.19 → 0.3.0-dev.20

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 (44) hide show
  1. package/dist/.tsbuildinfo.json +1 -1
  2. package/dist/effect/Schema/msgpack.d.ts +1 -1
  3. package/dist/effect/Schema/msgpack.d.ts.map +1 -1
  4. package/dist/effect/Subscribable.d.ts +2 -0
  5. package/dist/effect/Subscribable.d.ts.map +1 -1
  6. package/dist/effect/Subscribable.js +4 -3
  7. package/dist/effect/Subscribable.js.map +1 -1
  8. package/dist/effect/WebChannel/WebChannel.d.ts +19 -0
  9. package/dist/effect/WebChannel/WebChannel.d.ts.map +1 -1
  10. package/dist/effect/WebChannel/WebChannel.js +75 -1
  11. package/dist/effect/WebChannel/WebChannel.js.map +1 -1
  12. package/dist/effect/WebChannel/WebChannel.test.d.ts +2 -0
  13. package/dist/effect/WebChannel/WebChannel.test.d.ts.map +1 -0
  14. package/dist/effect/WebChannel/WebChannel.test.js +62 -0
  15. package/dist/effect/WebChannel/WebChannel.test.js.map +1 -0
  16. package/dist/effect/WebChannel/common.d.ts +2 -2
  17. package/dist/effect/WebChannel/common.d.ts.map +1 -1
  18. package/dist/effect/WebChannel/common.js +8 -19
  19. package/dist/effect/WebChannel/common.js.map +1 -1
  20. package/dist/effect/WebSocket.d.ts +1 -1
  21. package/dist/effect/index.d.ts +2 -1
  22. package/dist/effect/index.d.ts.map +1 -1
  23. package/dist/effect/index.js +2 -1
  24. package/dist/effect/index.js.map +1 -1
  25. package/dist/env.d.ts +1 -1
  26. package/dist/env.d.ts.map +1 -1
  27. package/dist/env.js +2 -2
  28. package/dist/env.js.map +1 -1
  29. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.d.ts +5 -5
  30. package/dist/node/ChildProcessRunner/ChildProcessWorker.js +1 -1
  31. package/dist/node/ChildProcessRunner/ChildProcessWorker.js.map +1 -1
  32. package/package.json +35 -27
  33. package/src/effect/Subscribable.ts +7 -0
  34. package/src/effect/WebChannel/WebChannel.test.ts +106 -0
  35. package/src/effect/WebChannel/WebChannel.ts +123 -1
  36. package/src/effect/WebChannel/common.ts +14 -21
  37. package/src/effect/index.ts +3 -0
  38. package/src/env.ts +2 -2
  39. package/src/node/ChildProcessRunner/ChildProcessWorker.ts +1 -1
  40. package/dist/nanoid/index.browser.d.ts +0 -2
  41. package/dist/nanoid/index.browser.d.ts.map +0 -1
  42. package/dist/nanoid/index.browser.js +0 -3
  43. package/dist/nanoid/index.browser.js.map +0 -1
  44. package/src/nanoid/index.browser.ts +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livestore/utils",
3
- "version": "0.3.0-dev.19",
3
+ "version": "0.3.0-dev.20",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "./dist/global.js",
@@ -19,7 +19,6 @@
19
19
  },
20
20
  "./nanoid": {
21
21
  "types": "./dist/nanoid/index.d.ts",
22
- "react-native": "./dist/nanoid/index.browser.js",
23
22
  "default": "./dist/nanoid/index.js"
24
23
  },
25
24
  "./effect": {
@@ -70,42 +69,51 @@
70
69
  }
71
70
  },
72
71
  "dependencies": {
72
+ "@standard-schema/spec": "1.0.0",
73
73
  "msgpackr": "1.11.2",
74
- "nanoid": "5.1.0",
74
+ "nanoid": "5.1.3",
75
75
  "pretty-bytes": "6.1.1"
76
76
  },
77
77
  "devDependencies": {
78
- "@effect/cli": "^0.56.1",
79
- "@effect/experimental": "^0.41.1",
80
- "@effect/opentelemetry": "^0.44.1",
81
- "@effect/platform": "^0.77.1",
82
- "@effect/platform-browser": "^0.56.1",
83
- "@effect/platform-bun": "^0.57.1",
84
- "@effect/platform-node": "^0.73.1",
85
- "@effect/vitest": "^0.18.1",
78
+ "@effect/cli": "^0.57.1",
79
+ "@effect/experimental": "^0.42.1",
80
+ "@effect/opentelemetry": "^0.44.8",
81
+ "@effect/platform": "^0.78.1",
82
+ "@effect/platform-browser": "^0.57.1",
83
+ "@effect/platform-bun": "^0.58.1",
84
+ "@effect/platform-node": "^0.74.1",
85
+ "@effect/printer": "^0.41.8",
86
+ "@effect/printer-ansi": "^0.41.8",
87
+ "@effect/typeclass": "^0.32.8",
88
+ "@effect/vitest": "^0.19.6",
86
89
  "@opentelemetry/api": "^1.9.0",
87
- "@opentelemetry/exporter-metrics-otlp-http": "^0.57.0",
90
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.57.2",
88
91
  "@opentelemetry/exporter-trace-otlp-http": "^0.57.2",
89
- "@opentelemetry/sdk-metrics": "^1.30.0",
92
+ "@opentelemetry/sdk-metrics": "^1.30.1",
90
93
  "@opentelemetry/sdk-trace-base": "^1.30.1",
91
- "@opentelemetry/sdk-trace-node": "^1.30.0",
92
- "@types/bun": "^1.1.12",
93
- "@types/node": "^22.10.10",
94
+ "@opentelemetry/sdk-trace-node": "^1.30.1",
95
+ "@types/bun": "^1.2.4",
96
+ "@types/jsdom": "^21.1.7",
97
+ "@types/node": "^22.13.10",
94
98
  "@types/web": "^0.0.203",
95
- "effect": "^3.13.1",
96
- "vitest": "^2.1.4"
99
+ "effect": "^3.13.8",
100
+ "jsdom": "^26.0.0",
101
+ "vitest": "^3.0.8"
97
102
  },
98
103
  "peerDependencies": {
99
- "@effect/cli": "~0.56.1",
100
- "@effect/experimental": "~0.41.1",
101
- "@effect/opentelemetry": "~0.44.1",
102
- "@effect/platform": "~0.77.1",
103
- "@effect/platform-browser": "~0.56.1",
104
- "@effect/platform-bun": "~0.57.1",
105
- "@effect/platform-node": "~0.73.1",
106
- "@effect/vitest": "~0.18.1",
104
+ "@effect/cli": "~0.57.1",
105
+ "@effect/experimental": "~0.42.1",
106
+ "@effect/opentelemetry": "~0.44.8",
107
+ "@effect/platform": "~0.78.1",
108
+ "@effect/platform-browser": "~0.57.1",
109
+ "@effect/platform-bun": "~0.58.1",
110
+ "@effect/platform-node": "~0.74.1",
111
+ "@effect/printer": "~0.41.8",
112
+ "@effect/printer-ansi": "~0.41.8",
113
+ "@effect/typeclass": "~0.32.8",
114
+ "@effect/vitest": "~0.19.6",
107
115
  "@opentelemetry/api": "~1.9.0",
108
- "effect": "~3.13.1"
116
+ "effect": "~3.13.8"
109
117
  },
110
118
  "publishConfig": {
111
119
  "access": "public"
@@ -4,6 +4,7 @@
4
4
  * @since 2.0.0
5
5
  */
6
6
 
7
+ import type { SubscriptionRef } from 'effect'
7
8
  import { Effect, Effectable, Readable, Stream } from 'effect'
8
9
  import { dual } from 'effect/Function'
9
10
  import { hasProperty } from 'effect/Predicate'
@@ -74,6 +75,12 @@ export const make = <A, E, R>(options: {
74
75
  readonly changes: Stream.Stream<A, E, R>
75
76
  }): Subscribable<A, E, R> => new SubscribableImpl(options.get as any, options.changes as any) as Subscribable<A, E, R>
76
77
 
78
+ export const fromSubscriptionRef = <A>(ref: SubscriptionRef.SubscriptionRef<A>): Subscribable<A> =>
79
+ make({
80
+ get: ref.get,
81
+ changes: ref.changes,
82
+ })
83
+
77
84
  /**
78
85
  * @since 2.0.0
79
86
  * @category combinators
@@ -0,0 +1,106 @@
1
+ import * as Vitest from '@effect/vitest'
2
+ import { Effect, Schema, Stream } from 'effect'
3
+ import { JSDOM } from 'jsdom'
4
+
5
+ import * as WebChannel from './WebChannel.js'
6
+
7
+ Vitest.describe('WebChannel', () => {
8
+ Vitest.describe('windowChannel', () => {
9
+ Vitest.scopedLive('should work with 2 windows', () =>
10
+ Effect.gen(function* () {
11
+ const windowA = new JSDOM().window as unknown as globalThis.Window
12
+ const windowB = new JSDOM().window as unknown as globalThis.Window
13
+
14
+ const codeSideA = Effect.gen(function* () {
15
+ const channelToB = yield* WebChannel.windowChannel2({
16
+ listenWindow: windowA,
17
+ sendWindow: windowB,
18
+ ids: { own: 'a', other: 'b' },
19
+ schema: Schema.Number,
20
+ })
21
+
22
+ const msgFromBFiber = yield* channelToB.listen.pipe(
23
+ Stream.flatten(),
24
+ Stream.runHead,
25
+ Effect.flatten,
26
+ Effect.fork,
27
+ )
28
+
29
+ yield* channelToB.send(1)
30
+
31
+ Vitest.expect(yield* msgFromBFiber).toEqual(2)
32
+ })
33
+
34
+ const codeSideB = Effect.gen(function* () {
35
+ const channelToA = yield* WebChannel.windowChannel2({
36
+ listenWindow: windowB,
37
+ sendWindow: windowA,
38
+ ids: { own: 'b', other: 'a' },
39
+ schema: Schema.Number,
40
+ })
41
+
42
+ const msgFromAFiber = yield* channelToA.listen.pipe(
43
+ Stream.flatten(),
44
+ Stream.runHead,
45
+ Effect.flatten,
46
+ Effect.fork,
47
+ )
48
+
49
+ yield* channelToA.send(2)
50
+
51
+ Vitest.expect(yield* msgFromAFiber).toEqual(1)
52
+ })
53
+
54
+ yield* Effect.all([codeSideA, codeSideB], { concurrency: 'unbounded' })
55
+ }),
56
+ )
57
+
58
+ Vitest.scopedLive('should work with the same window', () =>
59
+ Effect.gen(function* () {
60
+ const window = new JSDOM().window as unknown as globalThis.Window
61
+
62
+ const codeSideA = Effect.gen(function* () {
63
+ const channelToB = yield* WebChannel.windowChannel2({
64
+ listenWindow: window,
65
+ sendWindow: window,
66
+ ids: { own: 'a', other: 'b' },
67
+ schema: Schema.Number,
68
+ })
69
+
70
+ const msgFromBFiber = yield* channelToB.listen.pipe(
71
+ Stream.flatten(),
72
+ Stream.runHead,
73
+ Effect.flatten,
74
+ Effect.fork,
75
+ )
76
+
77
+ yield* channelToB.send(1)
78
+
79
+ Vitest.expect(yield* msgFromBFiber).toEqual(2)
80
+ })
81
+
82
+ const codeSideB = Effect.gen(function* () {
83
+ const channelToA = yield* WebChannel.windowChannel2({
84
+ listenWindow: window,
85
+ sendWindow: window,
86
+ ids: { own: 'b', other: 'a' },
87
+ schema: Schema.Number,
88
+ })
89
+
90
+ const msgFromAFiber = yield* channelToA.listen.pipe(
91
+ Stream.flatten(),
92
+ Stream.runHead,
93
+ Effect.flatten,
94
+ Effect.fork,
95
+ )
96
+
97
+ yield* channelToA.send(2)
98
+
99
+ Vitest.expect(yield* msgFromAFiber).toEqual(1)
100
+ })
101
+
102
+ yield* Effect.all([codeSideA, codeSideB], { concurrency: 'unbounded' })
103
+ }),
104
+ )
105
+ })
106
+ })
@@ -1,4 +1,4 @@
1
- import { Deferred, Either, Exit, Option, Queue, Scope } from 'effect'
1
+ import { Deferred, Either, Exit, GlobalValue, Option, PubSub, Queue, Scope } from 'effect'
2
2
 
3
3
  import * as Effect from '../Effect.js'
4
4
  import * as Schema from '../Schema/index.js'
@@ -27,6 +27,7 @@ export const noopChannel = <MsgListen, MsgSend>(): Effect.Effect<WebChannel<MsgL
27
27
  }).pipe(Effect.withSpan(`WebChannel:noopChannel`)),
28
28
  )
29
29
 
30
+ /** Only works in browser environments */
30
31
  export const broadcastChannel = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
31
32
  channelName,
32
33
  schema: inputSchema,
@@ -69,6 +70,84 @@ export const broadcastChannel = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEn
69
70
  }).pipe(Effect.withSpan(`WebChannel:broadcastChannel(${channelName})`)),
70
71
  )
71
72
 
73
+ /**
74
+ * NOTE the `listenName` and `sendName` is needed for cases where both sides are using the same window
75
+ * e.g. for a browser extension, so we need a way to know for which side a message is intended for.
76
+ */
77
+ export const windowChannel2 = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
78
+ listenWindow,
79
+ sendWindow,
80
+ targetOrigin = '*',
81
+ ids,
82
+ schema: inputSchema,
83
+ }: {
84
+ listenWindow: Window
85
+ sendWindow: Window
86
+ targetOrigin?: string
87
+ ids: { own: string; other: string }
88
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
89
+ }): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope> =>
90
+ Effect.scopeWithCloseable((scope) =>
91
+ Effect.gen(function* () {
92
+ const schema = mapSchema(inputSchema)
93
+
94
+ const debugInfo = {
95
+ sendTotal: 0,
96
+ listenTotal: 0,
97
+ targetOrigin,
98
+ ids,
99
+ }
100
+
101
+ const WindowMessageListen = Schema.Struct({
102
+ message: schema.listen,
103
+ from: Schema.Literal(ids.other),
104
+ to: Schema.Literal(ids.own),
105
+ }).annotations({ title: 'webmesh.WindowMessageListen' })
106
+
107
+ const WindowMessageSend = Schema.Struct({
108
+ message: schema.send,
109
+ from: Schema.Literal(ids.own),
110
+ to: Schema.Literal(ids.other),
111
+ }).annotations({ title: 'webmesh.WindowMessageSend' })
112
+
113
+ const send = (message: MsgSend) =>
114
+ Effect.gen(function* () {
115
+ debugInfo.sendTotal++
116
+
117
+ const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(WindowMessageSend)({
118
+ message,
119
+ from: ids.own,
120
+ to: ids.other,
121
+ })
122
+ sendWindow.postMessage(messageEncoded, targetOrigin, transferables)
123
+ })
124
+
125
+ const listen = Stream.fromEventListener<MessageEvent>(listenWindow, 'message').pipe(
126
+ // Stream.tap((_) => Effect.log(`${ids.other}→${ids.own}:message`, _.data)),
127
+ Stream.filter((_) => Schema.is(Schema.encodedSchema(WindowMessageListen))(_.data)),
128
+ Stream.map((_) => {
129
+ debugInfo.listenTotal++
130
+ return Schema.decodeEither(schema.listen)(_.data.message)
131
+ }),
132
+ listenToDebugPing('window'),
133
+ )
134
+
135
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
136
+ const supportsTransferables = true
137
+
138
+ return {
139
+ [WebChannelSymbol]: WebChannelSymbol,
140
+ send,
141
+ listen,
142
+ closedDeferred,
143
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
144
+ schema,
145
+ supportsTransferables,
146
+ debugInfo,
147
+ }
148
+ }).pipe(Effect.withSpan(`WebChannel:windowChannel`)),
149
+ )
150
+
72
151
  export const windowChannel = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
73
152
  window,
74
153
  targetOrigin = '*',
@@ -154,6 +233,49 @@ export const messagePortChannel: {
154
233
  }).pipe(Effect.withSpan(`WebChannel:messagePortChannel`)),
155
234
  )
156
235
 
236
+ const sameThreadChannels = GlobalValue.globalValue(
237
+ 'livestore:sameThreadChannels',
238
+ () => new Map<string, PubSub.PubSub<any>>(),
239
+ )
240
+
241
+ export const sameThreadChannel = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
242
+ schema: inputSchema,
243
+ channelName,
244
+ }: {
245
+ schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
246
+ channelName: string
247
+ }): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope> =>
248
+ Effect.scopeWithCloseable((scope) =>
249
+ Effect.gen(function* () {
250
+ let pubSub = sameThreadChannels.get(channelName)
251
+ if (pubSub === undefined) {
252
+ pubSub = yield* PubSub.unbounded<any>().pipe(Effect.acquireRelease(PubSub.shutdown))
253
+ sameThreadChannels.set(channelName, pubSub)
254
+ }
255
+
256
+ const schema = mapSchema(inputSchema)
257
+
258
+ const send = (message: MsgSend) =>
259
+ Effect.gen(function* () {
260
+ yield* PubSub.publish(pubSub, message)
261
+ })
262
+
263
+ const listen = Stream.fromPubSub(pubSub).pipe(Stream.map(Either.right), listenToDebugPing(channelName))
264
+
265
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
266
+
267
+ return {
268
+ [WebChannelSymbol]: WebChannelSymbol,
269
+ send,
270
+ listen,
271
+ closedDeferred,
272
+ shutdown: Scope.close(scope, Exit.succeed('shutdown')),
273
+ schema,
274
+ supportsTransferables: false,
275
+ }
276
+ }),
277
+ )
278
+
157
279
  export const messagePortChannelWithAck: {
158
280
  <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>(args: {
159
281
  port: MessagePort
@@ -1,5 +1,5 @@
1
- import type { Deferred, Effect, Either, ParseResult } from 'effect'
2
- import { Predicate, Schema, Stream } from 'effect'
1
+ import type { Deferred, Either, ParseResult } from 'effect'
2
+ import { Effect, Predicate, Schema, Stream } from 'effect'
3
3
 
4
4
  export const WebChannelSymbol = Symbol('WebChannel')
5
5
  export type WebChannelSymbol = typeof WebChannelSymbol
@@ -46,26 +46,19 @@ export const mapSchema = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>(
46
46
  ? (schemaWithDebugPing(schema) as any)
47
47
  : (schemaWithDebugPing({ send: schema, listen: schema }) as any)
48
48
 
49
- export const listenToDebugPing = (channelName: string) => {
50
- const threadName = (() => {
51
- if (typeof globalThis !== 'undefined' && Predicate.hasProperty(globalThis, 'name') && self.name !== '') {
52
- return self.name
53
- } else if (typeof globalThis !== 'undefined') {
54
- return 'window'
55
- }
56
- return 'unknown thread'
57
- })()
58
-
59
- return <MsgListen>(
49
+ export const listenToDebugPing =
50
+ (channelName: string) =>
51
+ <MsgListen>(
60
52
  stream: Stream.Stream<Either.Either<MsgListen, ParseResult.ParseError>, never>,
61
53
  ): Stream.Stream<Either.Either<MsgListen, ParseResult.ParseError>, never> =>
62
54
  stream.pipe(
63
- Stream.filter((msg) => {
64
- if (msg._tag === 'Right' && Schema.is(DebugPingMessage)(msg.right)) {
65
- console.log(`[${threadName}] WebChannel:ping [${channelName}]`, msg.right.message, msg.right.payload)
66
- return false
67
- }
68
- return true
69
- }),
55
+ Stream.filterEffect(
56
+ Effect.fn(function* (msg) {
57
+ if (msg._tag === 'Right' && Schema.is(DebugPingMessage)(msg.right)) {
58
+ yield* Effect.logDebug(`WebChannel:ping [${channelName}] ${msg.right.message}`, msg.right.payload)
59
+ return false
60
+ }
61
+ return true
62
+ }),
63
+ ),
70
64
  )
71
- }
@@ -59,8 +59,11 @@ export {
59
59
  ExecutionStrategy,
60
60
  PrimaryKey,
61
61
  Types,
62
+ Cache,
62
63
  } from 'effect'
63
64
 
65
+ export * as StandardSchema from '@standard-schema/spec'
66
+
64
67
  export { dual } from 'effect/Function'
65
68
 
66
69
  export * as Stream from './Stream.js'
package/src/env.ts CHANGED
@@ -26,8 +26,8 @@ export const isDevEnv = () => {
26
26
  return false
27
27
  }
28
28
 
29
- export const TRACE_VERBOSE = true
30
- // export const TRACE_VERBOSE = env('LS_TRACE_VERBOSE') !== undefined || env('VITE_LS_TRACE_VERBOSE') !== undefined
29
+ // export const TRACE_VERBOSE = true
30
+ export const TRACE_VERBOSE = env('LS_TRACE_VERBOSE') !== undefined || env('VITE_LS_TRACE_VERBOSE') !== undefined
31
31
 
32
32
  export const LS_DEV = env('LS_DEV') !== undefined || env('VITE_LS_DEV') !== undefined
33
33
 
@@ -23,8 +23,8 @@ const platformWorkerImpl = Worker.makePlatform<ChildProcess.ChildProcess>()({
23
23
  childProcess.send([1])
24
24
  return Deferred.await(exitDeferred)
25
25
  }).pipe(
26
- Effect.interruptible,
27
26
  Effect.timeout(5000),
27
+ Effect.interruptible,
28
28
  Effect.catchAllCause(() => Effect.sync(() => childProcess.kill())),
29
29
  ),
30
30
  ),
@@ -1,2 +0,0 @@
1
- export { nanoid } from 'nanoid/index.browser.js';
2
- //# sourceMappingURL=index.browser.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.browser.d.ts","sourceRoot":"","sources":["../../src/nanoid/index.browser.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA"}
@@ -1,3 +0,0 @@
1
- // @ts-expect-error TODO
2
- export { nanoid } from 'nanoid/index.browser.js';
3
- //# sourceMappingURL=index.browser.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.browser.js","sourceRoot":"","sources":["../../src/nanoid/index.browser.ts"],"names":[],"mappings":"AAAA,wBAAwB;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA"}
@@ -1,2 +0,0 @@
1
- // @ts-expect-error TODO
2
- export { nanoid } from 'nanoid/index.browser.js'