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.
- package/dist/internal/index.js +8 -8
- package/dist/internal/index.js.map +1 -1
- package/dist/introspection/index.d.ts.map +1 -1
- package/dist/main/index.d.ts +6 -4
- package/dist/main/index.d.ts.map +1 -1
- package/dist/main/index.js +4 -3
- package/dist/main/index.js.map +1 -1
- package/dist/realtime/index.d.ts +94 -31
- package/dist/realtime/index.d.ts.map +1 -1
- package/dist/realtime/index.js +34 -1
- package/dist/realtime/index.js.map +1 -1
- package/dist/realtime-react/index.d.ts +3 -1
- package/dist/realtime-react/index.d.ts.map +1 -1
- package/dist/realtime-react/index.js +7 -4
- package/dist/realtime-react/index.js.map +1 -1
- package/dist/realtime-server/index.d.ts +43 -13
- package/dist/realtime-server/index.d.ts.map +1 -1
- package/dist/realtime-server/index.js +120 -63
- package/dist/realtime-server/index.js.map +1 -1
- package/package.json +10 -10
- package/src/internal/families/create-readonly-held-selector-family.ts +2 -2
- package/src/internal/families/create-readonly-pure-selector-family.ts +2 -2
- package/src/internal/families/create-regular-atom-family.ts +2 -2
- package/src/internal/families/create-writable-held-selector-family.ts +2 -2
- package/src/internal/families/create-writable-pure-selector-family.ts +2 -2
- package/src/internal/mutable/create-mutable-atom-family.ts +2 -2
- package/src/internal/not-found-error.ts +2 -2
- package/src/main/logger.ts +8 -4
- package/src/realtime/cast-socket.ts +73 -0
- package/src/realtime/index.ts +2 -0
- package/src/realtime/socket-interface.ts +1 -1
- package/src/realtime/standard-schema.ts +72 -0
- package/src/realtime-react/index.ts +1 -0
- package/src/realtime-react/realtime-context.tsx +0 -9
- package/src/realtime-react/use-realtime-rooms.ts +13 -0
- package/src/realtime-server/realtime-server-stores/index.ts +1 -1
- package/src/realtime-server/realtime-server-stores/provide-rooms.ts +368 -0
- 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
|
+
}
|
package/src/realtime/index.ts
CHANGED
|
@@ -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 "./
|
|
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
|
+
}
|