atom.io 0.45.5 → 0.46.1

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 (31) hide show
  1. package/dist/internal/index.js +2 -7
  2. package/dist/internal/index.js.map +1 -1
  3. package/dist/react-devtools/index.d.ts.map +1 -1
  4. package/dist/realtime/index.d.ts +6 -3
  5. package/dist/realtime/index.d.ts.map +1 -1
  6. package/dist/realtime/index.js +9 -1
  7. package/dist/realtime/index.js.map +1 -1
  8. package/dist/realtime-server/index.d.ts +73 -50
  9. package/dist/realtime-server/index.d.ts.map +1 -1
  10. package/dist/realtime-server/index.js +222 -182
  11. package/dist/realtime-server/index.js.map +1 -1
  12. package/dist/realtime-testing/index.d.ts +1 -2
  13. package/dist/realtime-testing/index.d.ts.map +1 -1
  14. package/dist/realtime-testing/index.js +31 -53
  15. package/dist/realtime-testing/index.js.map +1 -1
  16. package/package.json +10 -10
  17. package/src/internal/subscribe/subscribe-to-state.ts +9 -18
  18. package/src/realtime/cast-socket.ts +1 -0
  19. package/src/realtime/shared-room-store.ts +15 -0
  20. package/src/realtime/socket-interface.ts +5 -1
  21. package/src/realtime-server/index.ts +3 -2
  22. package/src/realtime-server/ipc-sockets/custom-socket.ts +18 -2
  23. package/src/realtime-server/ipc-sockets/parent-socket.ts +1 -1
  24. package/src/realtime-server/{realtime-server-stores/provide-rooms.ts → provide-rooms.ts} +32 -57
  25. package/src/realtime-server/realtime-state-provider.ts +5 -3
  26. package/src/realtime-server/realtime-state-receiver.ts +19 -3
  27. package/src/realtime-server/server-config.ts +112 -1
  28. package/src/realtime-server/{realtime-server-stores/server-user-store.ts → server-socket-state.ts} +1 -8
  29. package/src/realtime-testing/setup-realtime-test.tsx +38 -83
  30. package/src/realtime-server/realtime-server-stores/index.ts +0 -3
  31. package/src/realtime-server/realtime-server-stores/provide-identity.ts +0 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atom.io",
3
- "version": "0.45.5",
3
+ "version": "0.46.1",
4
4
  "description": "Composable and testable reactive data library.",
5
5
  "homepage": "https://atom.io.fyi",
6
6
  "sideEffects": false,
@@ -60,19 +60,19 @@
60
60
  }
61
61
  },
62
62
  "devDependencies": {
63
- "@storybook/addon-docs": "10.1.4",
64
- "@storybook/addon-onboarding": "10.1.4",
65
- "@storybook/react-vite": "10.1.4",
63
+ "@storybook/addon-docs": "10.1.5",
64
+ "@storybook/addon-onboarding": "10.1.5",
65
+ "@storybook/react-vite": "10.1.5",
66
66
  "@testing-library/react": "16.3.0",
67
67
  "@types/eslint": "9.6.1",
68
68
  "@types/estree": "1.0.8",
69
69
  "@types/http-proxy": "1.17.17",
70
70
  "@types/react": "19.2.7",
71
71
  "@types/tmp": "0.2.6",
72
- "@typescript-eslint/parser": "8.48.1",
73
- "@typescript-eslint/rule-tester": "8.48.1",
74
- "@typescript-eslint/utils": "8.48.1",
75
- "@typescript/native-preview": "7.0.0-dev.20251208.1",
72
+ "@typescript-eslint/parser": "8.49.0",
73
+ "@typescript-eslint/rule-tester": "8.49.0",
74
+ "@typescript-eslint/utils": "8.49.0",
75
+ "@typescript/native-preview": "7.0.0-dev.20251209.1",
76
76
  "@vitest/coverage-v8": "4.0.15",
77
77
  "@vitest/ui": "4.0.15",
78
78
  "arktype": "2.1.28",
@@ -91,9 +91,9 @@
91
91
  "recoverage": "0.1.13",
92
92
  "socket.io": "4.8.1",
93
93
  "socket.io-client": "4.8.1",
94
- "storybook": "10.1.4",
94
+ "storybook": "10.1.5",
95
95
  "tmp": "0.2.5",
96
- "tsdown": "0.17.1",
96
+ "tsdown": "0.17.2",
97
97
  "vite": "7.2.7",
98
98
  "vite-tsconfig-paths": "5.1.4",
99
99
  "vitest": "4.0.15",
@@ -15,25 +15,16 @@ export function subscribeToState<T, E>(
15
15
  handleUpdate: UpdateHandler<E | T>,
16
16
  ): () => void {
17
17
  function safelyHandleUpdate(update: StateUpdate<any>): void {
18
- if (store.operation.open) {
19
- if (
20
- state?.type === `atom` &&
21
- hasRole(state, `tracker:signal`) &&
22
- `*` + store.operation.token.key === token.key &&
23
- `inboundTracker` in handleUpdate
24
- ) {
25
- return
26
- }
27
- const unsubscribe = store.on.operationClose.subscribe(
28
- `state subscription ${key}`,
29
- () => {
30
- unsubscribe()
31
- handleUpdate(update)
32
- },
33
- )
34
- } else {
35
- handleUpdate(update)
18
+ if (
19
+ store.operation.open &&
20
+ state?.type === `atom` &&
21
+ hasRole(state, `tracker:signal`) &&
22
+ `*` + store.operation.token.key === token.key &&
23
+ `inboundTracker` in handleUpdate
24
+ ) {
25
+ return
36
26
  }
27
+ handleUpdate(update)
37
28
  }
38
29
  reduceReference(store, token)
39
30
  const state = withdraw(store, token)
@@ -61,6 +61,7 @@ export function castSocket<T extends TypedSocket>(
61
61
  })
62
62
  })
63
63
  },
64
+ onAnyOutgoing: socket.onAnyOutgoing.bind(socket),
64
65
  off: socket.off.bind(socket),
65
66
  offAny: socket.offAny.bind(socket),
66
67
  emit: socket.emit.bind(socket),
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  JoinToken,
3
3
  MutableAtomToken,
4
+ PureSelectorFamilyToken,
4
5
  ReadonlyPureSelectorFamilyToken,
5
6
  } from "atom.io"
6
7
  import { getInternalRelations, join, mutableAtom, selectorFamily } from "atom.io"
@@ -41,6 +42,20 @@ export const usersInRooms: JoinToken<`room`, RoomKey, `user`, UserKey, `1:n`> =
41
42
  isBType: isUserKey,
42
43
  })
43
44
 
45
+ export const visibleUsersInRoomsSelector: PureSelectorFamilyToken<
46
+ (RoomKey | UserKey)[],
47
+ UserKey
48
+ > = selectorFamily({
49
+ key: `selfList`,
50
+ get:
51
+ (userKey) =>
52
+ ({ get }) => {
53
+ const [, roomsOfUsersAtoms] = getInternalRelations(usersInRooms, `split`)
54
+ const rooms = get(roomsOfUsersAtoms, userKey)
55
+ return [userKey, ...rooms]
56
+ },
57
+ })
58
+
44
59
  export const ownersOfRooms: JoinToken<`user`, UserKey, `room`, RoomKey, `1:n`> =
45
60
  join({
46
61
  key: `ownersOfRooms`,
@@ -28,11 +28,12 @@ export type EventEmitter<EmitEvents extends EventsMap = EventsMap> = <
28
28
 
29
29
  export type TypedSocket<
30
30
  ListenEvents extends EventsMap = EventsMap,
31
- EmitEvents extends EventsMap = EventsMap,
31
+ EmitEvents extends EventsMap = never,
32
32
  > = {
33
33
  id: string | undefined
34
34
  on: ParticularEventListener<ListenEvents>
35
35
  onAny: (listener: AllEventsListener<ListenEvents>) => void
36
+ onAnyOutgoing: (listener: AllEventsListener<EmitEvents>) => void
36
37
  off: ParticularEventListener<ListenEvents>
37
38
  offAny: (listener: AllEventsListener<ListenEvents>) => void
38
39
  emit: EventEmitter<EmitEvents>
@@ -44,6 +45,9 @@ export type Socket = {
44
45
  onAny: (
45
46
  listener: (event: string, ...args: Json.Serializable[]) => void,
46
47
  ) => void
48
+ onAnyOutgoing: (
49
+ listener: (event: string, ...args: Json.Serializable[]) => void,
50
+ ) => void
47
51
  off: (event: string, listener: (...args: Json.Serializable[]) => void) => void
48
52
  offAny: (
49
53
  listener: (event: string, ...args: Json.Serializable[]) => void,
@@ -1,9 +1,10 @@
1
1
  export * from "./continuity/provide-continuity"
2
2
  export * from "./ipc-sockets"
3
+ export * from "./provide-rooms"
3
4
  export * from "./realtime-family-provider"
4
5
  export * from "./realtime-mutable-family-provider"
5
6
  export * from "./realtime-mutable-provider"
6
- export * from "./realtime-server-stores"
7
7
  export * from "./realtime-state-provider"
8
8
  export * from "./realtime-state-receiver"
9
- export type * from "./server-config"
9
+ export * from "./server-config"
10
+ export * from "./server-socket-state"
@@ -20,6 +20,9 @@ export abstract class CustomSocket<I extends Events, O extends Events>
20
20
  {
21
21
  protected listeners: Map<keyof O, Set<(...args: Json.Array) => void>>
22
22
  protected globalListeners: Set<(event: string, ...args: Json.Array) => void>
23
+ protected globalListenersOutgoing: Set<
24
+ (event: string, ...args: Json.Array) => void
25
+ >
23
26
  protected handleEvent<K extends string & keyof I>(
24
27
  ...args: EventPayload<I, K>
25
28
  ): void {
@@ -36,7 +39,7 @@ export abstract class CustomSocket<I extends Events, O extends Events>
36
39
  }
37
40
 
38
41
  public id = `no_id_retrieved`
39
- public emit: <Event extends keyof I>(
42
+ public emit: <Event extends string & keyof I>(
40
43
  event: Event,
41
44
  ...args: I[Event]
42
45
  ) => CustomSocket<I, O>
@@ -47,9 +50,15 @@ export abstract class CustomSocket<I extends Events, O extends Events>
47
50
  ...args: I[Event]
48
51
  ) => CustomSocket<I, O>,
49
52
  ) {
50
- this.emit = emit
53
+ this.emit = (...args) => {
54
+ for (const listener of this.globalListenersOutgoing) {
55
+ listener(...args)
56
+ }
57
+ return emit(...args)
58
+ }
51
59
  this.listeners = new Map()
52
60
  this.globalListeners = new Set()
61
+ this.globalListenersOutgoing = new Set()
53
62
  }
54
63
 
55
64
  public on<Event extends keyof O>(
@@ -70,6 +79,13 @@ export abstract class CustomSocket<I extends Events, O extends Events>
70
79
  return this
71
80
  }
72
81
 
82
+ public onAnyOutgoing(
83
+ listener: (event: string, ...args: Json.Array) => void,
84
+ ): this {
85
+ this.globalListenersOutgoing.add(listener)
86
+ return this
87
+ }
88
+
73
89
  public off<Event extends keyof O>(
74
90
  event: Event,
75
91
  listener?: (...args: O[Event]) => void,
@@ -190,7 +190,7 @@ export class ParentSocket<
190
190
  relay.in.next(data)
191
191
  })
192
192
  relay.out.subscribe(`socket`, (data) => {
193
- this.emit(...(data as [string, ...I[keyof I]]))
193
+ this.emit(...(data as [string, ...I[string & keyof I]]))
194
194
  })
195
195
  })
196
196
 
@@ -19,7 +19,6 @@ import type {
19
19
  RoomSocketInterface,
20
20
  Socket,
21
21
  SocketGuard,
22
- SocketKey,
23
22
  StandardSchemaV1,
24
23
  TypedSocket,
25
24
  UserKey,
@@ -30,18 +29,13 @@ import {
30
29
  ownersOfRooms,
31
30
  roomKeysAtom,
32
31
  usersInRooms,
32
+ visibleUsersInRoomsSelector,
33
33
  } from "atom.io/realtime"
34
34
 
35
- import { ChildSocket } from "../ipc-sockets"
36
- import { realtimeMutableFamilyProvider } from "../realtime-mutable-family-provider"
37
- import { realtimeMutableProvider } from "../realtime-mutable-provider"
38
- import type { ServerConfig } from "../server-config"
39
- import {
40
- selfListSelectors,
41
- socketKeysAtom,
42
- userKeysAtom,
43
- usersOfSockets,
44
- } from "./server-user-store"
35
+ import { ChildSocket } from "./ipc-sockets"
36
+ import { realtimeMutableFamilyProvider } from "./realtime-mutable-family-provider"
37
+ import { realtimeMutableProvider } from "./realtime-mutable-provider"
38
+ import type { ServerConfig } from "./server-config"
45
39
 
46
40
  export type RoomMap = Map<
47
41
  string,
@@ -240,74 +234,55 @@ export function provideRooms<RoomNames extends string>({
240
234
  socket,
241
235
  resolveRoomScript,
242
236
  roomNames,
243
- }: ProvideRoomsConfig<RoomNames> & ServerConfig): void {
244
- const socketKey = `socket::${socket.id}` satisfies SocketKey
245
- const userKey = getFromStore(
246
- store,
247
- findRelationsInStore(store, usersOfSockets, socketKey).userKeyOfSocket,
248
- )!
249
- // const roomSocket = socket as TypedSocket<RoomSocketInterface<RoomNames>, {}>
250
- const roomSocket = castSocket<TypedSocket<RoomSocketInterface<RoomNames>, {}>>(
251
- socket,
252
- createRoomSocketGuard(roomNames),
253
- )
254
-
237
+ userKey,
238
+ }: ProvideRoomsConfig<RoomNames> & ServerConfig): () => void {
239
+ const roomSocket = castSocket<
240
+ TypedSocket<RoomSocketInterface<RoomNames>, never>
241
+ >(socket, createRoomSocketGuard(roomNames))
255
242
  const exposeMutable = realtimeMutableProvider({ socket, store, userKey })
256
- const exposeMutableFamily = realtimeMutableFamilyProvider({
257
- socket,
258
- store,
259
- userKey,
260
- })
261
-
262
- exposeMutable(roomKeysAtom)
263
-
264
- const [, usersInRoomsAtoms] = getInternalRelationsFromStore(
243
+ const unsubFromRoomKeys = exposeMutable(roomKeysAtom)
244
+ const usersInRoomsAtoms = getInternalRelationsFromStore(store, usersInRooms)
245
+ const [, usersInRoomsAtomsUsersOnly] = getInternalRelationsFromStore(
265
246
  store,
266
247
  usersInRooms,
267
248
  `split`,
268
249
  )
269
250
  const usersWhoseRoomsCanBeSeenSelector = findInStore(
270
251
  store,
271
- selfListSelectors,
252
+ visibleUsersInRoomsSelector,
272
253
  userKey,
273
254
  )
274
- exposeMutableFamily(usersInRoomsAtoms, usersWhoseRoomsCanBeSeenSelector)
275
- // const usersOfSocketsAtoms = getInternalRelationsFromStore(
276
- // store,
277
- // usersOfSockets,
278
- // )
279
- // exposeMutableFamily(usersOfSocketsAtoms, socketKeysAtom)
280
- const [ownersOfRoomsAtoms] = getInternalRelationsFromStore(
255
+ const ownersOfRoomsAtoms = getInternalRelationsFromStore(store, ownersOfRooms)
256
+ const exposeMutableFamily = realtimeMutableFamilyProvider({
257
+ socket,
281
258
  store,
282
- ownersOfRooms,
283
- `split`,
259
+ userKey,
260
+ })
261
+ const unsubFromUsersInRooms = exposeMutableFamily(
262
+ usersInRoomsAtoms,
263
+ usersWhoseRoomsCanBeSeenSelector,
264
+ )
265
+ const unsubFromOwnersOfRooms = exposeMutableFamily(
266
+ ownersOfRoomsAtoms,
267
+ usersWhoseRoomsCanBeSeenSelector,
284
268
  )
285
- exposeMutableFamily(ownersOfRoomsAtoms, usersWhoseRoomsCanBeSeenSelector)
286
-
287
269
  const enterRoom = provideEnterAndExit({ store, socket, roomSocket, userKey })
288
270
 
289
- const userRoomSet = getFromStore(store, usersInRoomsAtoms, userKey)
271
+ const userRoomSet = getFromStore(store, usersInRoomsAtomsUsersOnly, userKey)
290
272
  for (const userRoomKey of userRoomSet) {
291
273
  enterRoom(userRoomKey)
292
274
  break
293
275
  }
294
-
295
276
  roomSocket.on(
296
277
  `createRoom`,
297
278
  spawnRoom({ store, socket, userKey, resolveRoomScript }),
298
279
  )
299
280
  roomSocket.on(`deleteRoom`, destroyRoom({ store, socket, userKey }))
300
- socket.on(`disconnect`, () => {
301
- store.logger.info(
302
- `📡`,
303
- `socket`,
304
- socket.id ?? `[ID MISSING?!]`,
305
- `👤 ${userKey} disconnects`,
306
- )
307
- editRelationsInStore(store, usersOfSockets, (rel) => rel.delete(socketKey))
308
- setIntoStore(store, userKeysAtom, (keys) => (keys.delete(userKey), keys))
309
- setIntoStore(store, socketKeysAtom, (keys) => (keys.delete(socketKey), keys))
310
- })
281
+ return () => {
282
+ unsubFromRoomKeys()
283
+ unsubFromUsersInRooms()
284
+ unsubFromOwnersOfRooms()
285
+ }
311
286
  }
312
287
 
313
288
  const roomKeySchema: StandardSchemaV1<Json.Array, [RoomKey]> = {
@@ -21,9 +21,11 @@ export function realtimeStateProvider({
21
21
  store = IMPLICIT.STORE,
22
22
  }: ServerConfig) {
23
23
  store.logger.info(`🔌`, `user`, userKey, `initialized state provider`)
24
- return function stateProvider<J extends Json.Serializable>(
25
- clientToken: AtomIO.WritableToken<J>,
26
- serverData: AtomIO.ReadableToken<NoInfer<J>> | NoInfer<J> = clientToken,
24
+ return function stateProvider<C extends Json.Serializable, S extends C>(
25
+ clientToken: AtomIO.WritableToken<C>,
26
+ serverData:
27
+ | AtomIO.ReadableToken<S>
28
+ | S = clientToken as AtomIO.ReadableToken<S>,
27
29
  ): () => void {
28
30
  const isStatic = !isReadableToken(serverData)
29
31
 
@@ -9,6 +9,7 @@ import {
9
9
  subscribeToState,
10
10
  } from "atom.io/internal"
11
11
  import type { Json } from "atom.io/json"
12
+ import type { SocketKey, StandardSchemaV1 } from "atom.io/realtime"
12
13
  import { employSocket, mutexAtoms } from "atom.io/realtime"
13
14
 
14
15
  import type { ServerConfig } from "."
@@ -16,12 +17,15 @@ import type { ServerConfig } from "."
16
17
  export type StateReceiver = ReturnType<typeof realtimeStateReceiver>
17
18
  export function realtimeStateReceiver({
18
19
  socket,
20
+ userKey,
19
21
  store = IMPLICIT.STORE,
20
22
  }: ServerConfig) {
21
23
  return function stateReceiver<S extends Json.Serializable, C extends S>(
24
+ schema: StandardSchemaV1<unknown, C>,
22
25
  clientToken: WritableToken<C>,
23
26
  serverToken: WritableToken<S> = clientToken,
24
27
  ): () => void {
28
+ const socketKey = `socket::${socket.id}` satisfies SocketKey
25
29
  const mutexAtom = findInStore(store, mutexAtoms, serverToken.key)
26
30
 
27
31
  const subscriptions = new Set<() => void>()
@@ -33,8 +37,20 @@ export function realtimeStateReceiver({
33
37
  const permitPublish = () => {
34
38
  clearSubscriptions()
35
39
  subscriptions.add(
36
- employSocket(socket, `pub:${clientToken.key}`, (newValue) => {
37
- setIntoStore(store, serverToken, newValue as C)
40
+ employSocket(socket, `pub:${clientToken.key}`, async (newValue) => {
41
+ const parsed = await schema[`~standard`].validate(newValue)
42
+ if (parsed.issues) {
43
+ store.logger.error(
44
+ `❌`,
45
+ `user`,
46
+ userKey,
47
+ `attempted to publish invalid value`,
48
+ newValue,
49
+ `to state "${serverToken.key}"`,
50
+ )
51
+ return
52
+ }
53
+ setIntoStore(store, serverToken, parsed.value)
38
54
  }),
39
55
  )
40
56
  subscriptions.add(
@@ -52,7 +68,7 @@ export function realtimeStateReceiver({
52
68
  if (getFromStore(store, mutexAtom)) {
53
69
  clearSubscriptions()
54
70
  subscriptions.add(
55
- subscribeToState(store, mutexAtom, socket.id!, () => {
71
+ subscribeToState(store, mutexAtom, socketKey, () => {
56
72
  const currentValue = getFromStore(store, mutexAtom)
57
73
  if (currentValue === false) {
58
74
  operateOnStore(OWN_OP, store, mutexAtom, true)
@@ -1,8 +1,119 @@
1
+ import type { IncomingHttpHeaders } from "node:http"
2
+ import type { ParsedUrlQuery } from "node:querystring"
3
+
4
+ import { Realm } from "atom.io"
1
5
  import type { RootStore } from "atom.io/internal"
2
- import type { Socket, UserKey } from "atom.io/realtime"
6
+ import {
7
+ editRelationsInStore,
8
+ findInStore,
9
+ findRelationsInStore,
10
+ getFromStore,
11
+ IMPLICIT,
12
+ setIntoStore,
13
+ } from "atom.io/internal"
14
+ import type { Socket, SocketKey, UserKey } from "atom.io/realtime"
15
+ import type { Server } from "socket.io"
16
+
17
+ import { realtimeStateProvider } from "./realtime-state-provider"
18
+ import type { SocketSystemHierarchy } from "./server-socket-state"
19
+ import {
20
+ socketAtoms,
21
+ socketKeysAtom,
22
+ userKeysAtom,
23
+ usersOfSockets,
24
+ } from "./server-socket-state"
3
25
 
4
26
  export type ServerConfig = {
5
27
  socket: Socket
6
28
  userKey: UserKey
7
29
  store?: RootStore
8
30
  }
31
+
32
+ /** Socket Handshake details--taken from socket.io */
33
+ export type Handshake = {
34
+ /** The headers sent as part of the handshake */
35
+ headers: IncomingHttpHeaders
36
+ /** The date of creation (as string) */
37
+ time: string
38
+ /** The ip of the client */
39
+ address: string
40
+ /** Whether the connection is cross-domain */
41
+ xdomain: boolean
42
+ /** Whether the connection is secure */
43
+ secure: boolean
44
+ /** The date of creation (as unix timestamp) */
45
+ issued: number
46
+ /** The request URL string */
47
+ url: string
48
+ /** The query object */
49
+ query: ParsedUrlQuery
50
+ /** The auth object */
51
+ auth: {
52
+ [key: string]: any
53
+ }
54
+ }
55
+
56
+ export function realtime(
57
+ server: Server,
58
+ auth: (handshake: Handshake) => Error | UserKey,
59
+ onConnect: (config: ServerConfig) => () => void,
60
+ store: RootStore = IMPLICIT.STORE,
61
+ ): () => Promise<void> {
62
+ const socketRealm = new Realm<SocketSystemHierarchy>(store)
63
+
64
+ server
65
+ .use((socket, next) => {
66
+ const result = auth(socket.handshake)
67
+ if (result instanceof Error) {
68
+ next(result)
69
+ return
70
+ }
71
+ const userClaim = socketRealm.allocate(`root`, result)
72
+ const socketClaim = socketRealm.allocate(`root`, `socket::${socket.id}`)
73
+ const socketState = findInStore(store, socketAtoms, socketClaim)
74
+ setIntoStore(store, socketState, socket)
75
+ editRelationsInStore(store, usersOfSockets, (relations) => {
76
+ relations.set(userClaim, socketClaim)
77
+ })
78
+ setIntoStore(store, userKeysAtom, (index) => index.add(userClaim))
79
+ setIntoStore(store, socketKeysAtom, (index) => index.add(socketClaim))
80
+ next()
81
+ })
82
+ .on(`connection`, (socket) => {
83
+ const socketKey = `socket::${socket.id}` satisfies SocketKey
84
+ const userKeyState = findRelationsInStore(
85
+ store,
86
+ usersOfSockets,
87
+ socketKey,
88
+ ).userKeyOfSocket
89
+ const userKey = getFromStore(store, userKeyState)!
90
+ const serverConfig: ServerConfig = { store, socket, userKey }
91
+ const provideState = realtimeStateProvider(serverConfig)
92
+ const unsubFromMyUserKey = provideState(
93
+ { key: `myUserKey`, type: `atom` },
94
+ userKey,
95
+ )
96
+
97
+ const disposeServices = onConnect(serverConfig)
98
+
99
+ socket.on(`disconnect`, () => {
100
+ store.logger.info(`📡`, `socket`, socketKey, `👤 ${userKey} disconnects`)
101
+ disposeServices()
102
+ unsubFromMyUserKey()
103
+ editRelationsInStore(store, usersOfSockets, (rel) =>
104
+ rel.delete(socketKey),
105
+ )
106
+ setIntoStore(store, userKeysAtom, (keys) => (keys.delete(userKey), keys))
107
+ setIntoStore(
108
+ store,
109
+ socketKeysAtom,
110
+ (keys) => (keys.delete(socketKey), keys),
111
+ )
112
+ })
113
+ })
114
+
115
+ const disposeAll = async () => {
116
+ await server.close()
117
+ }
118
+ return disposeAll
119
+ }
@@ -2,10 +2,9 @@ import type {
2
2
  Hierarchy,
3
3
  JoinToken,
4
4
  MutableAtomToken,
5
- PureSelectorFamilyToken,
6
5
  RegularAtomFamilyToken,
7
6
  } from "atom.io"
8
- import { atomFamily, join, mutableAtom, selectorFamily } from "atom.io"
7
+ import { atomFamily, join, mutableAtom } from "atom.io"
9
8
  import type { RoomKey, Socket, SocketKey, UserKey } from "atom.io/realtime"
10
9
  import { isSocketKey, isUserKey } from "atom.io/realtime"
11
10
  import { UList } from "atom.io/transceivers/u-list"
@@ -46,9 +45,3 @@ export const usersOfSockets: JoinToken<
46
45
  isAType: isUserKey,
47
46
  isBType: isSocketKey,
48
47
  })
49
-
50
- export const selfListSelectors: PureSelectorFamilyToken<UserKey[], UserKey> =
51
- selectorFamily({
52
- key: `selfList`,
53
- get: (userKey) => () => [userKey],
54
- })