atom.io 0.44.12 → 0.44.13

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 (38) hide show
  1. package/dist/internal/index.js +8 -8
  2. package/dist/internal/index.js.map +1 -1
  3. package/dist/introspection/index.d.ts.map +1 -1
  4. package/dist/main/index.d.ts +6 -4
  5. package/dist/main/index.d.ts.map +1 -1
  6. package/dist/main/index.js +4 -3
  7. package/dist/main/index.js.map +1 -1
  8. package/dist/realtime/index.d.ts +94 -31
  9. package/dist/realtime/index.d.ts.map +1 -1
  10. package/dist/realtime/index.js +34 -1
  11. package/dist/realtime/index.js.map +1 -1
  12. package/dist/realtime-react/index.d.ts +3 -1
  13. package/dist/realtime-react/index.d.ts.map +1 -1
  14. package/dist/realtime-react/index.js +7 -4
  15. package/dist/realtime-react/index.js.map +1 -1
  16. package/dist/realtime-server/index.d.ts +43 -13
  17. package/dist/realtime-server/index.d.ts.map +1 -1
  18. package/dist/realtime-server/index.js +120 -63
  19. package/dist/realtime-server/index.js.map +1 -1
  20. package/package.json +10 -10
  21. package/src/internal/families/create-readonly-held-selector-family.ts +2 -2
  22. package/src/internal/families/create-readonly-pure-selector-family.ts +2 -2
  23. package/src/internal/families/create-regular-atom-family.ts +2 -2
  24. package/src/internal/families/create-writable-held-selector-family.ts +2 -2
  25. package/src/internal/families/create-writable-pure-selector-family.ts +2 -2
  26. package/src/internal/mutable/create-mutable-atom-family.ts +2 -2
  27. package/src/internal/not-found-error.ts +2 -2
  28. package/src/main/logger.ts +8 -4
  29. package/src/realtime/cast-socket.ts +73 -0
  30. package/src/realtime/index.ts +2 -0
  31. package/src/realtime/socket-interface.ts +1 -1
  32. package/src/realtime/standard-schema.ts +72 -0
  33. package/src/realtime-react/index.ts +1 -0
  34. package/src/realtime-react/realtime-context.tsx +0 -9
  35. package/src/realtime-react/use-realtime-rooms.ts +13 -0
  36. package/src/realtime-server/realtime-server-stores/index.ts +1 -1
  37. package/src/realtime-server/realtime-server-stores/provide-rooms.ts +368 -0
  38. package/src/realtime-server/realtime-server-stores/server-room-external-store.ts +0 -227
@@ -0,0 +1,73 @@
1
+ import type { Loadable } from "atom.io"
2
+ import type { Json } from "atom.io/json"
3
+
4
+ import type { EventsMap, Socket, TypedSocket } from "./socket-interface"
5
+ import type { StandardSchemaV1 } from "./standard-schema"
6
+
7
+ export type SocketListeners<T extends TypedSocket> = T extends TypedSocket<
8
+ infer ListenEvents
9
+ >
10
+ ? ListenEvents
11
+ : never
12
+
13
+ export type SocketGuard<L extends EventsMap> = {
14
+ [K in keyof L]: StandardSchemaV1<Json.Array, Parameters<L[K]>>
15
+ }
16
+
17
+ export type Loaded<L extends Loadable<any>> = L extends Loadable<infer T>
18
+ ? T
19
+ : never
20
+
21
+ function onLoad<L extends Loadable<any>>(
22
+ loadable: L,
23
+ fn: (loaded: Loaded<L>) => any,
24
+ ): void {
25
+ if (loadable instanceof Promise) {
26
+ void loadable.then(fn)
27
+ } else {
28
+ fn(loadable as Loaded<L>)
29
+ }
30
+ }
31
+
32
+ export function castSocket<T extends TypedSocket>(
33
+ socket: Socket,
34
+ guard: SocketGuard<SocketListeners<T>> | `TRUST`,
35
+ logError?: (error: unknown) => void,
36
+ ): T {
37
+ if (guard === `TRUST`) {
38
+ return socket as T
39
+ }
40
+ const guardedSocket: Socket = {
41
+ id: socket.id,
42
+ on: (event, listener) => {
43
+ const schema = guard[event] as StandardSchemaV1<Json.Array, Json.Array>
44
+ socket.on(event, (...args) => {
45
+ const loadableResult = schema[`~standard`].validate(args)
46
+ onLoad(loadableResult, (result) => {
47
+ if (result.issues) {
48
+ logError?.(result.issues)
49
+ } else {
50
+ listener(...result.value)
51
+ }
52
+ })
53
+ })
54
+ },
55
+ onAny: (listener) => {
56
+ socket.onAny((event, ...args) => {
57
+ const schema = guard[event] as StandardSchemaV1<unknown, Json.Array>
58
+ const loadableResult = schema[`~standard`].validate(args)
59
+ onLoad(loadableResult, (result) => {
60
+ if (result.issues) {
61
+ logError?.(result.issues)
62
+ } else {
63
+ listener(event, ...result.value)
64
+ }
65
+ })
66
+ })
67
+ },
68
+ off: socket.off.bind(socket),
69
+ offAny: socket.offAny.bind(socket),
70
+ emit: socket.emit.bind(socket),
71
+ }
72
+ return guardedSocket as T
73
+ }
@@ -1,6 +1,8 @@
1
+ export * from "./cast-socket"
1
2
  export * from "./employ-socket"
2
3
  export * from "./mutex-store"
3
4
  export * from "./realtime-continuity"
4
5
  export * from "./realtime-key-types"
5
6
  export * from "./shared-room-store"
6
7
  export type * from "./socket-interface"
8
+ export type * from "./standard-schema"
@@ -20,7 +20,7 @@ export type AllEventsListener<ListenEvents extends EventsMap = EventsMap> = <
20
20
  ) => void
21
21
 
22
22
  export type EventEmitter<EmitEvents extends EventsMap = EventsMap> = <
23
- E extends keyof EmitEvents,
23
+ E extends string & keyof EmitEvents,
24
24
  >(
25
25
  event: E,
26
26
  ...args: Parameters<EmitEvents[E]>
@@ -0,0 +1,72 @@
1
+ /* eslint-disable quotes */
2
+
3
+ /** The Standard Schema interface. */
4
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
5
+ /** The Standard Schema properties. */
6
+ readonly "~standard": StandardSchemaV1.Props<Input, Output>
7
+ }
8
+
9
+ export declare namespace StandardSchemaV1 {
10
+ /** The Standard Schema properties interface. */
11
+ export interface Props<Input = unknown, Output = Input> {
12
+ /** The version number of the standard. */
13
+ readonly version: 1
14
+ /** The vendor name of the schema library. */
15
+ readonly vendor: string
16
+ /** Validates unknown input values. */
17
+ readonly validate: (
18
+ value: unknown,
19
+ ) => Promise<Result<Output>> | Result<Output>
20
+ /** Inferred types associated with the schema. */
21
+ readonly types?: Types<Input, Output> | undefined
22
+ }
23
+
24
+ /** The result interface of the validate function. */
25
+ export type Result<Output> = FailureResult | SuccessResult<Output>
26
+
27
+ /** The result interface if validation succeeds. */
28
+ export interface SuccessResult<Output> {
29
+ /** The typed output value. */
30
+ readonly value: Output
31
+ /** The non-existent issues. */
32
+ readonly issues?: undefined
33
+ }
34
+
35
+ /** The result interface if validation fails. */
36
+ export interface FailureResult {
37
+ /** The issues of failed validation. */
38
+ readonly issues: ReadonlyArray<Issue>
39
+ }
40
+
41
+ /** The issue interface of the failure output. */
42
+ export interface Issue {
43
+ /** The error message of the issue. */
44
+ readonly message: string
45
+ /** The path of the issue, if any. */
46
+ readonly path?: ReadonlyArray<PathSegment | PropertyKey> | undefined
47
+ }
48
+
49
+ /** The path segment interface of the issue. */
50
+ export interface PathSegment {
51
+ /** The key representing a path segment. */
52
+ readonly key: PropertyKey
53
+ }
54
+
55
+ /** The Standard Schema types interface. */
56
+ export interface Types<Input = unknown, Output = Input> {
57
+ /** The input type of the schema. */
58
+ readonly input: Input
59
+ /** The output type of the schema. */
60
+ readonly output: Output
61
+ }
62
+
63
+ /** Infers the input type of a Standard Schema. */
64
+ export type InferInput<Schema extends StandardSchemaV1> = NonNullable<
65
+ Schema["~standard"]["types"]
66
+ >["input"]
67
+
68
+ /** Infers the output type of a Standard Schema. */
69
+ export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<
70
+ Schema["~standard"]["types"]
71
+ >["output"]
72
+ }
@@ -6,6 +6,7 @@ export * from "./use-pull-mutable-family-member"
6
6
  export * from "./use-pull-selector"
7
7
  export * from "./use-pull-selector-family-member"
8
8
  export * from "./use-push"
9
+ export * from "./use-realtime-rooms"
9
10
  export * from "./use-realtime-service"
10
11
  export * from "./use-single-effect"
11
12
  export * from "./use-sync-continuity"
@@ -1,5 +1,4 @@
1
1
  import { useI } from "atom.io/react"
2
- import type { RoomSocketInterface } from "atom.io/realtime"
3
2
  import * as RTC from "atom.io/realtime-client"
4
3
  import * as React from "react"
5
4
  import type { Socket } from "socket.io-client"
@@ -43,11 +42,3 @@ export const RealtimeProvider: React.FC<{
43
42
  </RealtimeContext.Provider>
44
43
  )
45
44
  }
46
-
47
- export function useRealtimeRooms<RoomNames extends string>(): Socket<
48
- {},
49
- RoomSocketInterface<RoomNames>
50
- > {
51
- const { socket } = React.useContext(RealtimeContext)
52
- return socket as Socket<{}, RoomSocketInterface<RoomNames>>
53
- }
@@ -0,0 +1,13 @@
1
+ import type { RoomSocketInterface } from "atom.io/realtime"
2
+ import * as React from "react"
3
+ import type { Socket } from "socket.io-client"
4
+
5
+ import { RealtimeContext } from "./realtime-context"
6
+
7
+ export function useRealtimeRooms<RoomNames extends string>(): Socket<
8
+ {},
9
+ RoomSocketInterface<RoomNames>
10
+ > {
11
+ const { socket } = React.useContext(RealtimeContext)
12
+ return socket as Socket<{}, RoomSocketInterface<RoomNames>>
13
+ }
@@ -1,2 +1,2 @@
1
- export * from "./server-room-external-store"
1
+ export * from "./provide-rooms"
2
2
  export * from "./server-user-store"
@@ -0,0 +1,368 @@
1
+ import type { ChildProcessWithoutNullStreams } from "node:child_process"
2
+ import { spawn } from "node:child_process"
3
+
4
+ import type { RootStore } from "atom.io/internal"
5
+ import {
6
+ editRelationsInStore,
7
+ findInStore,
8
+ findRelationsInStore,
9
+ getFromStore,
10
+ getInternalRelationsFromStore,
11
+ IMPLICIT,
12
+ setIntoStore,
13
+ } from "atom.io/internal"
14
+ import type { Json } from "atom.io/json"
15
+ import type {
16
+ AllEventsListener,
17
+ EventsMap,
18
+ RoomKey,
19
+ RoomSocketInterface,
20
+ Socket,
21
+ SocketGuard,
22
+ SocketKey,
23
+ StandardSchemaV1,
24
+ TypedSocket,
25
+ UserKey,
26
+ } from "atom.io/realtime"
27
+ import {
28
+ castSocket,
29
+ isRoomKey,
30
+ ownersOfRooms,
31
+ roomKeysAtom,
32
+ usersInRooms,
33
+ } from "atom.io/realtime"
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"
45
+
46
+ export type RoomMap = Map<
47
+ string,
48
+ ChildSocket<any, any, ChildProcessWithoutNullStreams>
49
+ >
50
+
51
+ declare global {
52
+ var ATOM_IO_REALTIME_SERVER_ROOMS: RoomMap
53
+ }
54
+ export const ROOMS: RoomMap =
55
+ globalThis.ATOM_IO_REALTIME_SERVER_ROOMS ??
56
+ (globalThis.ATOM_IO_REALTIME_SERVER_ROOMS = new Map())
57
+
58
+ export const roomMeta: { count: number } = { count: 0 }
59
+
60
+ export type SpawnRoomConfig<RoomNames extends string> = {
61
+ store: RootStore
62
+ socket: Socket
63
+ userKey: UserKey
64
+ resolveRoomScript: (roomName: RoomNames) => [string, string[]]
65
+ }
66
+ export function spawnRoom<RoomNames extends string>({
67
+ store,
68
+ socket,
69
+ userKey,
70
+ resolveRoomScript,
71
+ }: SpawnRoomConfig<RoomNames>): (
72
+ roomName: RoomNames,
73
+ ) => Promise<ChildSocket<any, any>> {
74
+ return async (roomName) => {
75
+ store.logger.info(
76
+ `📡`,
77
+ `socket`,
78
+ socket.id ?? `[ID MISSING?!]`,
79
+ `👤 ${userKey} spawns room ${roomName}`,
80
+ )
81
+ const roomKey = `room::${roomMeta.count++}` satisfies RoomKey
82
+ const [command, args] = resolveRoomScript(roomName)
83
+ const child = await new Promise<ChildProcessWithoutNullStreams>(
84
+ (resolve) => {
85
+ const room = spawn(command, args, { env: process.env })
86
+ const resolver = (data: Buffer) => {
87
+ if (data.toString() === `ALIVE`) {
88
+ room.stdout.off(`data`, resolver)
89
+ resolve(room)
90
+ }
91
+ }
92
+ room.stdout.on(`data`, resolver)
93
+ },
94
+ )
95
+ const roomSocket = new ChildSocket(child, roomKey)
96
+ ROOMS.set(roomKey, roomSocket)
97
+ setIntoStore(store, roomKeysAtom, (index) => (index.add(roomKey), index))
98
+
99
+ editRelationsInStore(store, ownersOfRooms, (relations) => {
100
+ relations.set({ room: roomKey, user: userKey })
101
+ })
102
+
103
+ roomSocket.on(`close`, () => {
104
+ destroyRoom({ store, socket, userKey })(roomKey)
105
+ })
106
+
107
+ return roomSocket
108
+ }
109
+ }
110
+
111
+ export type ProvideEnterAndExitConfig = {
112
+ store: RootStore
113
+ socket: Socket
114
+ roomSocket: TypedSocket<RoomSocketInterface<any>, any>
115
+ userKey: UserKey
116
+ }
117
+ export function provideEnterAndExit({
118
+ store,
119
+ socket,
120
+ roomSocket,
121
+ userKey,
122
+ }: ProvideEnterAndExitConfig): (roomKey: RoomKey) => void {
123
+ const enterRoom = (roomKey: RoomKey) => {
124
+ store.logger.info(
125
+ `📡`,
126
+ `socket`,
127
+ socket.id ?? `[ID MISSING?!]`,
128
+ `👤 ${userKey} enters room ${roomKey}`,
129
+ )
130
+
131
+ const exitRoom = () => {
132
+ store.logger.info(
133
+ `📡`,
134
+ `socket`,
135
+ socket.id ?? `[ID MISSING?!]`,
136
+ `👤 ${userKey} leaves room ${roomKey}`,
137
+ )
138
+ socket.offAny(forward)
139
+ toRoom([`user-leaves`])
140
+ editRelationsInStore(store, usersInRooms, (relations) => {
141
+ relations.delete({ room: roomKey, user: userKey })
142
+ })
143
+ roomSocket.off(`leaveRoom`, exitRoom)
144
+ roomSocket.on(`joinRoom`, enterRoom)
145
+ }
146
+
147
+ roomSocket.on(`leaveRoom`, exitRoom)
148
+ roomSocket.off(`joinRoom`, enterRoom)
149
+
150
+ const roomQueue: [string, ...Json.Array][] = []
151
+ const pushToRoomQueue = (payload: [string, ...Json.Array]): void => {
152
+ roomQueue.push(payload)
153
+ }
154
+ let toRoom = pushToRoomQueue
155
+ const forward: AllEventsListener<EventsMap> = (...payload) => {
156
+ toRoom(payload)
157
+ }
158
+ socket.onAny(forward)
159
+
160
+ editRelationsInStore(store, usersInRooms, (relations) => {
161
+ relations.set({ room: roomKey, user: userKey })
162
+ })
163
+ const childSocket = ROOMS.get(roomKey)
164
+ if (!childSocket) {
165
+ store.logger.error(`❌`, `unknown`, roomKey, `no room found with this id`)
166
+ return null
167
+ }
168
+ childSocket.onAny((...payload) => {
169
+ socket.emit(...payload)
170
+ })
171
+ childSocket.emit(`user-joins`, userKey)
172
+
173
+ toRoom = (payload) => {
174
+ childSocket.emit(`user::${userKey}`, ...payload)
175
+ }
176
+ while (roomQueue.length > 0) {
177
+ const payload = roomQueue.shift()
178
+ if (payload) toRoom(payload)
179
+ }
180
+ }
181
+ roomSocket.on(`joinRoom`, enterRoom)
182
+ return enterRoom
183
+ }
184
+
185
+ export type DestroyRoomConfig = {
186
+ store: RootStore
187
+ socket: Socket
188
+ userKey: UserKey
189
+ }
190
+ export function destroyRoom({
191
+ store,
192
+ socket,
193
+ userKey,
194
+ }: DestroyRoomConfig): (roomKey: RoomKey) => void {
195
+ return (roomKey: RoomKey) => {
196
+ store.logger.info(
197
+ `📡`,
198
+ `socket`,
199
+ socket.id ?? `[ID MISSING?!]`,
200
+ `👤 ${userKey} attempts to delete room ${roomKey}`,
201
+ )
202
+ const owner = getFromStore(
203
+ store,
204
+ findRelationsInStore(store, ownersOfRooms, roomKey).userKeyOfRoom,
205
+ )
206
+ if (owner === userKey) {
207
+ store.logger.info(
208
+ `📡`,
209
+ `socket`,
210
+ socket.id ?? `[ID MISSING?!]`,
211
+ `👤 ${userKey} deletes room ${roomKey}`,
212
+ )
213
+ setIntoStore(store, roomKeysAtom, (s) => (s.delete(roomKey), s))
214
+ editRelationsInStore(store, usersInRooms, (relations) => {
215
+ relations.delete({ room: roomKey })
216
+ })
217
+ const room = ROOMS.get(roomKey)
218
+ if (room) {
219
+ room.emit(`exit`)
220
+ ROOMS.delete(roomKey)
221
+ }
222
+ return
223
+ }
224
+ store.logger.info(
225
+ `📡`,
226
+ `socket`,
227
+ socket.id ?? `[ID MISSING?!]`,
228
+ `👤 ${userKey} failed to delete room ${roomKey}; room owner is ${owner}`,
229
+ )
230
+ }
231
+ }
232
+
233
+ export type ProvideRoomsConfig<RoomNames extends string> = {
234
+ resolveRoomScript: (path: RoomNames) => [string, string[]]
235
+ roomNames: RoomNames[]
236
+ roomTimeLimit?: number
237
+ }
238
+ export function provideRooms<RoomNames extends string>({
239
+ store = IMPLICIT.STORE,
240
+ socket,
241
+ resolveRoomScript,
242
+ 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
+
255
+ const exposeMutable = realtimeMutableProvider({ socket, store })
256
+ const exposeMutableFamily = realtimeMutableFamilyProvider({
257
+ socket,
258
+ store,
259
+ })
260
+
261
+ exposeMutable(roomKeysAtom)
262
+
263
+ const [, usersInRoomsAtoms] = getInternalRelationsFromStore(
264
+ store,
265
+ usersInRooms,
266
+ `split`,
267
+ )
268
+ const usersWhoseRoomsCanBeSeenSelector = findInStore(
269
+ store,
270
+ selfListSelectors,
271
+ userKey,
272
+ )
273
+ exposeMutableFamily(usersInRoomsAtoms, usersWhoseRoomsCanBeSeenSelector)
274
+ const usersOfSocketsAtoms = getInternalRelationsFromStore(
275
+ store,
276
+ usersOfSockets,
277
+ )
278
+ exposeMutableFamily(usersOfSocketsAtoms, socketKeysAtom)
279
+
280
+ const enterRoom = provideEnterAndExit({ store, socket, roomSocket, userKey })
281
+
282
+ const userRoomSet = getFromStore(store, usersInRoomsAtoms, userKey)
283
+ for (const userRoomKey of userRoomSet) {
284
+ enterRoom(userRoomKey)
285
+ break
286
+ }
287
+
288
+ roomSocket.on(
289
+ `createRoom`,
290
+ spawnRoom({ store, socket, userKey, resolveRoomScript }),
291
+ )
292
+ roomSocket.on(`deleteRoom`, destroyRoom({ store, socket, userKey }))
293
+ socket.on(`disconnect`, () => {
294
+ store.logger.info(
295
+ `📡`,
296
+ `socket`,
297
+ socket.id ?? `[ID MISSING?!]`,
298
+ `👤 ${userKey} disconnects`,
299
+ )
300
+ editRelationsInStore(store, usersOfSockets, (rel) => rel.delete(socketKey))
301
+ setIntoStore(store, userKeysAtom, (keys) => (keys.delete(userKey), keys))
302
+ setIntoStore(store, socketKeysAtom, (keys) => (keys.delete(socketKey), keys))
303
+ })
304
+ }
305
+
306
+ const roomKeySchema: StandardSchemaV1<Json.Array, [RoomKey]> = {
307
+ "~standard": {
308
+ version: 1,
309
+ vendor: `atom.io`,
310
+ validate: ([maybeRoomKey]: Json.Array) => {
311
+ if (typeof maybeRoomKey === `string`) {
312
+ if (isRoomKey(maybeRoomKey)) {
313
+ return { value: [maybeRoomKey] }
314
+ }
315
+ return {
316
+ issues: [
317
+ {
318
+ message: `Room key must start with "room::"`,
319
+ },
320
+ ],
321
+ }
322
+ }
323
+ return {
324
+ issues: [
325
+ {
326
+ message: `Room key must be a string`,
327
+ },
328
+ ],
329
+ }
330
+ },
331
+ },
332
+ }
333
+
334
+ function createRoomSocketGuard<RoomNames extends string>(
335
+ roomNames: RoomNames[],
336
+ ): SocketGuard<RoomSocketInterface<RoomNames>> {
337
+ return {
338
+ createRoom: {
339
+ "~standard": {
340
+ version: 1,
341
+ vendor: `atom.io`,
342
+ validate: ([maybeRoomName]) => {
343
+ if (roomNames.includes(maybeRoomName as RoomNames)) {
344
+ return { value: [maybeRoomName as RoomNames] }
345
+ }
346
+ return {
347
+ issues: [
348
+ {
349
+ message:
350
+ `Room name must be one of the following:\n - ` +
351
+ roomNames.join(`\n - `),
352
+ },
353
+ ],
354
+ }
355
+ },
356
+ },
357
+ },
358
+ joinRoom: roomKeySchema,
359
+ deleteRoom: roomKeySchema,
360
+ leaveRoom: {
361
+ "~standard": {
362
+ version: 1,
363
+ vendor: `atom.io`,
364
+ validate: () => ({ value: [] }),
365
+ },
366
+ },
367
+ }
368
+ }