@holo-js/flux 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -13,11 +13,13 @@ type ManifestPresenceMember<TManifest extends GeneratedBroadcastManifest, TPatte
13
13
  member: infer TMember;
14
14
  } ? TMember : BroadcastJsonObject;
15
15
  type ManifestWhisperName<TManifest extends GeneratedBroadcastManifest, TPattern extends string> = ManifestChannelEntryByPattern<TManifest, TPattern>['whispers'][number] & string;
16
- type ManifestEventNamesForPattern<TManifest extends GeneratedBroadcastManifest, TPattern extends string> = Extract<TManifest['events'][number], {
17
- channels: readonly {
18
- pattern: TPattern;
16
+ type ManifestEventNamesForPattern<TManifest extends GeneratedBroadcastManifest, TPattern extends string> = TManifest['events'][number] extends infer TEvent ? TEvent extends {
17
+ readonly name: infer TName;
18
+ readonly channels: readonly {
19
+ readonly pattern: infer TEventPattern;
19
20
  }[];
20
- }>['name'] & string;
21
+ } ? TPattern extends TEventPattern & string ? TName & string : never : never : never;
22
+ type ManifestSubscriptionEventName<TManifest extends GeneratedBroadcastManifest, TChannel extends string> = string extends ManifestEventName<TManifest> ? string : TChannel extends ManifestChannelPattern<TManifest> ? ManifestEventNamesForPattern<TManifest, TChannel> : ManifestEventName<TManifest>;
21
23
  interface FluxClientOptions<TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest> {
22
24
  readonly manifest?: TManifest;
23
25
  readonly connection?: string;
@@ -33,6 +35,11 @@ interface FluxListenerControls {
33
35
  interface FluxPresenceState<TMember = unknown> {
34
36
  readonly members: readonly TMember[];
35
37
  }
38
+ interface FluxPresenceListenerControls<TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest, TChannel extends string = string> {
39
+ here(callback: (members: readonly ManifestPresenceMember<TManifest, TChannel>[]) => void): FluxPresenceSubscription<TManifest, TChannel>;
40
+ joining(callback: (member: ManifestPresenceMember<TManifest, TChannel>) => void): FluxPresenceSubscription<TManifest, TChannel>;
41
+ leaving(callback: (member: ManifestPresenceMember<TManifest, TChannel>) => void): FluxPresenceSubscription<TManifest, TChannel>;
42
+ }
36
43
  interface FluxConnectionControls {
37
44
  connect(): Promise<void>;
38
45
  disconnect(): Promise<void>;
@@ -60,12 +67,12 @@ interface FluxConnector {
60
67
  interface FluxSubscription<TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest, TChannel extends string = string> extends FluxListenerControls {
61
68
  readonly name: TChannel;
62
69
  readonly type: FluxChannelKind;
63
- listen<TEvent extends ManifestEventNamesForPattern<TManifest, TChannel> | ManifestEventName<TManifest>>(event?: TEvent | readonly TEvent[], callback?: (payload: BroadcastJsonObject) => void): FluxSubscription<TManifest, TChannel>;
70
+ listen<TEvent extends ManifestSubscriptionEventName<TManifest, TChannel>>(event?: TEvent | readonly TEvent[], callback?: (payload: BroadcastJsonObject) => void): FluxSubscription<TManifest, TChannel>;
64
71
  notification(callback: (payload: BroadcastJsonObject) => void): FluxSubscription<TManifest, TChannel>;
65
72
  listenForWhisper<TWhisper extends ManifestWhisperName<TManifest, TChannel>>(name: TWhisper, callback: (payload: BroadcastJsonObject) => void): FluxSubscription<TManifest, TChannel>;
66
73
  whisper<TWhisper extends ManifestWhisperName<TManifest, TChannel>>(name: TWhisper, payload: BroadcastJsonObject): Promise<void>;
67
74
  }
68
- interface FluxPresenceSubscription<TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest, TChannel extends string = string> extends FluxSubscription<TManifest, TChannel>, FluxPresenceState<ManifestPresenceMember<TManifest, TChannel>> {
75
+ interface FluxPresenceSubscription<TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest, TChannel extends string = string> extends FluxSubscription<TManifest, TChannel>, FluxPresenceState<ManifestPresenceMember<TManifest, TChannel>>, FluxPresenceListenerControls<TManifest, TChannel> {
69
76
  }
70
77
  interface FluxClient<TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest> extends FluxConnectionControls {
71
78
  readonly options: Readonly<FluxClientOptions<TManifest>>;
@@ -94,7 +101,7 @@ declare function createSubscription<TManifest extends GeneratedBroadcastManifest
94
101
  readonly __onPresenceChange: (callback: (members: readonly BroadcastJsonObject[]) => void) => () => void;
95
102
  };
96
103
  declare function createPresenceSubscription<TManifest extends GeneratedBroadcastManifest, TChannel extends string>(name: TChannel, connector: FluxConnector, registry: SubscriptionRegistry): FluxPresenceSubscription<TManifest, TChannel>;
97
- declare function createFluxClient<TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest>(options?: FluxClientOptions<TManifest>): FluxClient<TManifest> & ConnectorDebugCarrier;
104
+ declare function createFluxClient<const TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest>(options?: FluxClientOptions<TManifest>): FluxClient<TManifest> & ConnectorDebugCarrier;
98
105
  declare function configureFluxClient(options: FluxClientOptions | FluxClient): FluxClient;
99
106
  declare function getFluxClient(): FluxClient;
100
107
  declare function resetFluxClient(): void;
@@ -106,4 +113,4 @@ declare const fluxInternals: {
106
113
  createSubscription: typeof createSubscription;
107
114
  };
108
115
 
109
- export { type FluxChannelKind, type FluxClient, type FluxClientOptions, type FluxConnectionControls, type FluxConnectionStatus, type FluxConnector, type FluxConnectorChannel, type FluxListenerControls, type FluxPresenceState, type FluxPresenceSubscription, type FluxSubscription, configureFluxClient, createFluxClient, flux as default, flux, fluxInternals, getFluxClient, resetFluxClient };
116
+ export { type FluxChannelKind, type FluxClient, type FluxClientOptions, type FluxConnectionControls, type FluxConnectionStatus, type FluxConnector, type FluxConnectorChannel, type FluxListenerControls, type FluxPresenceListenerControls, type FluxPresenceState, type FluxPresenceSubscription, type FluxSubscription, configureFluxClient, createFluxClient, flux as default, flux, fluxInternals, getFluxClient, resetFluxClient };
package/dist/index.mjs CHANGED
@@ -9,6 +9,40 @@ function normalizeRequiredString(value, label) {
9
9
  function toReadonlyArray(value) {
10
10
  return Array.isArray(value) ? [...value] : [value];
11
11
  }
12
+ function presenceMemberKey(member) {
13
+ return JSON.stringify(member) ?? String(member);
14
+ }
15
+ function presenceMemberDiff(previousMembers, nextMembers) {
16
+ const previousCounts = /* @__PURE__ */ new Map();
17
+ const nextCounts = /* @__PURE__ */ new Map();
18
+ for (const member of previousMembers) {
19
+ const key = presenceMemberKey(member);
20
+ previousCounts.set(key, (previousCounts.get(key) ?? 0) + 1);
21
+ }
22
+ for (const member of nextMembers) {
23
+ const key = presenceMemberKey(member);
24
+ nextCounts.set(key, (nextCounts.get(key) ?? 0) + 1);
25
+ }
26
+ const joining = nextMembers.filter((member) => {
27
+ const key = presenceMemberKey(member);
28
+ const previousCount = previousCounts.get(key) ?? 0;
29
+ if (previousCount > 0) {
30
+ previousCounts.set(key, previousCount - 1);
31
+ return false;
32
+ }
33
+ return true;
34
+ });
35
+ const leaving = previousMembers.filter((member) => {
36
+ const key = presenceMemberKey(member);
37
+ const nextCount = nextCounts.get(key) ?? 0;
38
+ if (nextCount > 0) {
39
+ nextCounts.set(key, nextCount - 1);
40
+ return false;
41
+ }
42
+ return true;
43
+ });
44
+ return { joining, leaving };
45
+ }
12
46
  function addCallback(map, event, callback) {
13
47
  const listeners = map.get(event) ?? /* @__PURE__ */ new Set();
14
48
  listeners.add(callback);
@@ -256,8 +290,8 @@ function createSubscription(channelName, kind, connector, registry) {
256
290
  registeredSubscriptions.delete(leaveChannel);
257
291
  if (registeredSubscriptions.size === 0) {
258
292
  registry.delete(registryKey);
293
+ connectorChannel.leave();
259
294
  }
260
- connectorChannel.leave();
261
295
  };
262
296
  const leaveRelated = () => {
263
297
  for (const leave of [...registry.get(registryKey) ?? []]) {
@@ -296,19 +330,92 @@ function createSubscription(channelName, kind, connector, registry) {
296
330
  return connectorChannel.members;
297
331
  },
298
332
  __onPresenceChange(callback) {
299
- return connectorChannel.onMembersChange(callback);
333
+ const stop = connectorChannel.onMembersChange(callback);
334
+ detachCallbacks.add(stop);
335
+ return () => {
336
+ detachCallbacks.delete(stop);
337
+ stop();
338
+ };
300
339
  }
301
340
  };
302
341
  return Object.freeze(subscription);
303
342
  }
304
343
  function createPresenceSubscription(name, connector, registry) {
305
344
  const base = createSubscription(name, "presence", connector, registry);
306
- return Object.freeze({
307
- ...base,
345
+ const joiningCallbacks = /* @__PURE__ */ new Set();
346
+ const leavingCallbacks = /* @__PURE__ */ new Set();
347
+ let active = true;
348
+ let previousMembers = base.__presenceMembers();
349
+ const stopPresenceChanges = base.__onPresenceChange((nextMembers) => {
350
+ if (!active) {
351
+ previousMembers = nextMembers;
352
+ return;
353
+ }
354
+ const diff = presenceMemberDiff(previousMembers, nextMembers);
355
+ previousMembers = nextMembers;
356
+ for (const member of diff.joining) {
357
+ for (const callback of joiningCallbacks) {
358
+ callback(member);
359
+ }
360
+ }
361
+ for (const member of diff.leaving) {
362
+ for (const callback of leavingCallbacks) {
363
+ callback(member);
364
+ }
365
+ }
366
+ });
367
+ const stopPresenceSubscription = () => {
368
+ active = false;
369
+ joiningCallbacks.clear();
370
+ leavingCallbacks.clear();
371
+ stopPresenceChanges();
372
+ };
373
+ const subscription = Object.freeze({
374
+ name: base.name,
375
+ type: base.type,
308
376
  get members() {
309
377
  return base.__presenceMembers();
378
+ },
379
+ here(callback) {
380
+ callback(subscription.members);
381
+ return subscription;
382
+ },
383
+ joining(callback) {
384
+ joiningCallbacks.add(callback);
385
+ return subscription;
386
+ },
387
+ leaving(callback) {
388
+ leavingCallbacks.add(callback);
389
+ return subscription;
390
+ },
391
+ leaveChannel() {
392
+ stopPresenceSubscription();
393
+ base.leaveChannel();
394
+ },
395
+ leave() {
396
+ stopPresenceSubscription();
397
+ base.leave();
398
+ },
399
+ stopListening() {
400
+ active = false;
401
+ base.stopListening();
402
+ },
403
+ listen(event, callback) {
404
+ active = true;
405
+ base.listen(event, callback);
406
+ return subscription;
407
+ },
408
+ notification(callback) {
409
+ return base.notification(callback);
410
+ },
411
+ listenForWhisper(name2, callback) {
412
+ return base.listenForWhisper(name2, callback);
413
+ },
414
+ async whisper(name2, payload) {
415
+ await base.whisper(name2, payload);
310
416
  }
311
417
  });
418
+ return subscription;
312
419
  }
313
420
  function createFluxClient(options = {}) {
314
421
  const connector = options.connector ?? options.connectorFactory?.(options) ?? createUnavailableConnector();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holo-js/flux",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Holo-JS Framework - framework-agnostic realtime client skeleton for broadcast and presence",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,16 +19,16 @@
19
19
  "scripts": {
20
20
  "build": "tsup",
21
21
  "stub": "tsup",
22
- "typecheck": "tsc -p tsconfig.json --noEmit",
22
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.type-tests.json --noEmit",
23
23
  "test": "vitest --run"
24
24
  },
25
25
  "dependencies": {
26
- "@holo-js/broadcast": "^0.1.3"
26
+ "@holo-js/broadcast": "^0.1.5"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.10.2",
30
30
  "tsup": "^8.3.5",
31
31
  "typescript": "^5.7.2",
32
- "vitest": "^2.1.8"
32
+ "vitest": "^4.1.5"
33
33
  }
34
34
  }