atom.io 0.18.0 → 0.18.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.
- package/dist/{chunk-OEVFAUPE.js → chunk-IZHOMSXA.js} +53 -11
- package/dist/chunk-IZHOMSXA.js.map +1 -0
- package/dist/chunk-JDUNWJFB.js +18 -0
- package/dist/chunk-JDUNWJFB.js.map +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js.map +1 -1
- package/internal/dist/index.cjs +64 -34
- package/internal/dist/index.cjs.map +1 -1
- package/internal/dist/index.d.ts +1 -1
- package/internal/dist/index.js +64 -34
- package/internal/dist/index.js.map +1 -1
- package/internal/src/atom/delete-atom.ts +7 -6
- package/internal/src/caching.ts +6 -6
- package/internal/src/ingest-updates/ingest-atom-update.ts +6 -2
- package/internal/src/set-state/copy-mutable-if-needed.ts +5 -0
- package/internal/src/set-state/emit-update.ts +25 -11
- package/internal/src/set-state/set-atom.ts +4 -3
- package/internal/src/transaction/apply-transaction.ts +0 -1
- package/internal/src/transaction/set-epoch-number.ts +0 -1
- package/json/src/index.ts +3 -3
- package/package.json +241 -241
- package/react-devtools/dist/index.cjs.map +1 -1
- package/react-devtools/dist/index.js +1 -15
- package/react-devtools/dist/index.js.map +1 -1
- package/react-devtools/src/StateEditor.tsx +6 -6
- package/react-devtools/src/StateIndex.tsx +2 -2
- package/react-devtools/src/Updates.tsx +1 -1
- package/react-devtools/src/index.ts +3 -3
- package/realtime/dist/index.cjs +50 -2
- package/realtime/dist/index.cjs.map +1 -1
- package/realtime/dist/index.d.ts +110 -3
- package/realtime/dist/index.js +47 -4
- package/realtime/dist/index.js.map +1 -1
- package/realtime/src/index.ts +1 -0
- package/realtime/src/realtime-continuity.ts +14 -4
- package/realtime/src/shared-room-store.ts +48 -0
- package/realtime-client/dist/index.cjs +113 -200
- package/realtime-client/dist/index.cjs.map +1 -1
- package/realtime-client/dist/index.d.ts +2 -5
- package/realtime-client/dist/index.js +17 -161
- package/realtime-client/dist/index.js.map +1 -1
- package/realtime-client/src/index.ts +0 -2
- package/realtime-client/src/pull-mutable-atom-family-member.ts +5 -5
- package/realtime-client/src/realtime-client-stores/client-main-store.ts +10 -0
- package/realtime-client/src/sync-continuity.ts +56 -9
- package/realtime-react/dist/index.cjs +51 -26
- package/realtime-react/dist/index.cjs.map +1 -1
- package/realtime-react/dist/index.d.ts +2 -6
- package/realtime-react/dist/index.js +2 -17
- package/realtime-react/dist/index.js.map +1 -1
- package/realtime-react/src/index.ts +0 -2
- package/realtime-server/dist/index.cjs +399 -327
- package/realtime-server/dist/index.cjs.map +1 -1
- package/realtime-server/dist/index.d.ts +55 -60
- package/realtime-server/dist/index.js +394 -319
- package/realtime-server/dist/index.js.map +1 -1
- package/realtime-server/src/index.ts +2 -4
- package/realtime-server/src/ipc-sockets/child-socket.ts +135 -0
- package/realtime-server/src/ipc-sockets/custom-socket.ts +90 -0
- package/realtime-server/src/ipc-sockets/index.ts +3 -0
- package/realtime-server/src/ipc-sockets/parent-socket.ts +185 -0
- package/realtime-server/src/realtime-continuity-synchronizer.ts +225 -96
- package/realtime-server/src/realtime-server-stores/index.ts +2 -1
- package/realtime-server/src/realtime-server-stores/realtime-continuity-store.ts +50 -31
- package/realtime-server/src/realtime-server-stores/server-room-external-actions.ts +64 -0
- package/realtime-server/src/realtime-server-stores/server-room-external-store.ts +42 -0
- package/realtime-server/src/realtime-server-stores/server-sync-store.ts +49 -26
- package/realtime-testing/dist/index.cjs +8 -6
- package/realtime-testing/dist/index.cjs.map +1 -1
- package/realtime-testing/dist/index.d.ts +1 -0
- package/realtime-testing/dist/index.js +7 -6
- package/realtime-testing/dist/index.js.map +1 -1
- package/realtime-testing/src/setup-realtime-test.tsx +8 -6
- package/src/logger.ts +5 -1
- package/dist/chunk-OEVFAUPE.js.map +0 -1
- package/realtime-client/src/sync-server-action.ts +0 -168
- package/realtime-client/src/sync-state.ts +0 -19
- package/realtime-react/src/use-sync-server-action.ts +0 -17
- package/realtime-react/src/use-sync.ts +0 -17
- package/realtime-server/src/ipc-socket.ts +0 -230
- package/realtime-server/src/realtime-action-synchronizer.ts +0 -164
- package/realtime-server/src/realtime-server-stores/server-room-store.ts +0 -97
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import type * as AtomIO from "atom.io"
|
|
2
|
-
import * as Internal from "atom.io/internal"
|
|
3
|
-
import type { Socket } from "socket.io-client"
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
confirmedUpdateQueue,
|
|
7
|
-
optimisticUpdateQueue,
|
|
8
|
-
} from "./realtime-client-stores"
|
|
9
|
-
|
|
10
|
-
export function syncAction<ƒ extends AtomIO.ƒn>(
|
|
11
|
-
token: AtomIO.TransactionToken<ƒ>,
|
|
12
|
-
socket: Socket,
|
|
13
|
-
store: Internal.Store,
|
|
14
|
-
): () => void {
|
|
15
|
-
Internal.assignTransactionToContinuity(`default`, token.key, store)
|
|
16
|
-
|
|
17
|
-
const optimisticQueue = Internal.getFromStore(optimisticUpdateQueue, store)
|
|
18
|
-
const confirmedQueue = Internal.getFromStore(confirmedUpdateQueue, store)
|
|
19
|
-
|
|
20
|
-
const unsubscribeFromLocalUpdates = Internal.subscribeToTransaction(
|
|
21
|
-
token,
|
|
22
|
-
(clientUpdate) => {
|
|
23
|
-
const optimisticUpdateQueueIndex = optimisticQueue.findIndex(
|
|
24
|
-
(update) => update.id === clientUpdate.id,
|
|
25
|
-
)
|
|
26
|
-
if (optimisticUpdateQueueIndex === -1) {
|
|
27
|
-
Internal.setIntoStore(
|
|
28
|
-
optimisticUpdateQueue,
|
|
29
|
-
(queue) => {
|
|
30
|
-
queue.push(clientUpdate)
|
|
31
|
-
queue.sort((a, b) => a.epoch - b.epoch)
|
|
32
|
-
return queue
|
|
33
|
-
},
|
|
34
|
-
store,
|
|
35
|
-
)
|
|
36
|
-
} else {
|
|
37
|
-
Internal.setIntoStore(
|
|
38
|
-
optimisticUpdateQueue,
|
|
39
|
-
(queue) => {
|
|
40
|
-
queue[optimisticUpdateQueueIndex] = clientUpdate
|
|
41
|
-
return queue
|
|
42
|
-
},
|
|
43
|
-
store,
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
socket.emit(`tx-run:${token.key}`, clientUpdate)
|
|
47
|
-
},
|
|
48
|
-
`tx-run:${token.key}`,
|
|
49
|
-
store,
|
|
50
|
-
)
|
|
51
|
-
const reconcileUpdates = (
|
|
52
|
-
optimisticUpdate: AtomIO.TransactionUpdate<ƒ>,
|
|
53
|
-
confirmedUpdate: AtomIO.TransactionUpdate<ƒ>,
|
|
54
|
-
) => {
|
|
55
|
-
Internal.setIntoStore(
|
|
56
|
-
optimisticUpdateQueue,
|
|
57
|
-
(queue) => {
|
|
58
|
-
queue.shift()
|
|
59
|
-
return queue
|
|
60
|
-
},
|
|
61
|
-
store,
|
|
62
|
-
)
|
|
63
|
-
if (optimisticUpdate.id === confirmedUpdate.id) {
|
|
64
|
-
const clientResult = JSON.stringify(optimisticUpdate.updates)
|
|
65
|
-
const serverResult = JSON.stringify(confirmedUpdate.updates)
|
|
66
|
-
if (clientResult === serverResult) {
|
|
67
|
-
store.logger.info(
|
|
68
|
-
`✅`,
|
|
69
|
-
`transaction`,
|
|
70
|
-
token.key,
|
|
71
|
-
`results for ${optimisticUpdate.id} match between client and server`,
|
|
72
|
-
)
|
|
73
|
-
socket.emit(`tx-ack:${token.key}`, confirmedUpdate.epoch)
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
} else {
|
|
77
|
-
// id mismatch
|
|
78
|
-
store.logger.info(
|
|
79
|
-
`❌`,
|
|
80
|
-
`transaction`,
|
|
81
|
-
token.key,
|
|
82
|
-
`${store.config.name} thought update #${confirmedUpdate.epoch} was ${optimisticUpdate.key}:${optimisticUpdate.id}, but it was actually ${confirmedUpdate.key}:${confirmedUpdate.id}`,
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
-
for (const subsequentOptimistic of optimisticQueue.toReversed()) {
|
|
86
|
-
Internal.ingestTransactionUpdate(`oldValue`, subsequentOptimistic, store)
|
|
87
|
-
}
|
|
88
|
-
Internal.ingestTransactionUpdate(`oldValue`, optimisticUpdate, store)
|
|
89
|
-
Internal.ingestTransactionUpdate(`newValue`, confirmedUpdate, store)
|
|
90
|
-
socket.emit(`tx-ack:${token.key}`, confirmedUpdate.epoch)
|
|
91
|
-
for (const subsequentOptimistic of optimisticQueue) {
|
|
92
|
-
const token = Object.assign(
|
|
93
|
-
{ type: `transaction` } as const,
|
|
94
|
-
subsequentOptimistic,
|
|
95
|
-
)
|
|
96
|
-
const { id, params } = subsequentOptimistic
|
|
97
|
-
Internal.actUponStore(token, id, store)(...params)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const registerAndAttemptConfirmedUpdate = (
|
|
102
|
-
confirmedUpdate: AtomIO.TransactionUpdate<ƒ>,
|
|
103
|
-
) => {
|
|
104
|
-
const zerothOptimisticUpdate = optimisticQueue[0]
|
|
105
|
-
if (zerothOptimisticUpdate) {
|
|
106
|
-
if (zerothOptimisticUpdate.epoch === confirmedUpdate.epoch) {
|
|
107
|
-
reconcileUpdates(zerothOptimisticUpdate, confirmedUpdate)
|
|
108
|
-
for (const nextConfirmed of confirmedQueue) {
|
|
109
|
-
const nextOptimistic = optimisticQueue[0]
|
|
110
|
-
if (nextConfirmed.epoch === nextOptimistic.epoch) {
|
|
111
|
-
reconcileUpdates(nextOptimistic, nextConfirmed)
|
|
112
|
-
} else {
|
|
113
|
-
break
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
} else {
|
|
117
|
-
// epoch mismatch
|
|
118
|
-
|
|
119
|
-
const hasEnqueuedOptimisticUpdate = optimisticQueue.some(
|
|
120
|
-
(update) => update.epoch === confirmedUpdate.epoch,
|
|
121
|
-
)
|
|
122
|
-
if (hasEnqueuedOptimisticUpdate) {
|
|
123
|
-
Internal.setIntoStore(
|
|
124
|
-
confirmedUpdateQueue,
|
|
125
|
-
(queue) => {
|
|
126
|
-
queue.push(confirmedUpdate)
|
|
127
|
-
queue.sort((a, b) => a.epoch - b.epoch)
|
|
128
|
-
return queue
|
|
129
|
-
},
|
|
130
|
-
store,
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
} else {
|
|
135
|
-
const continuityEpoch = Internal.getEpochNumberOfAction(token.key, store)
|
|
136
|
-
if (
|
|
137
|
-
Internal.isRootStore(store) &&
|
|
138
|
-
continuityEpoch === confirmedUpdate.epoch - 1
|
|
139
|
-
) {
|
|
140
|
-
Internal.ingestTransactionUpdate(`newValue`, confirmedUpdate, store)
|
|
141
|
-
socket.emit(`tx-ack:${token.key}`, confirmedUpdate.epoch)
|
|
142
|
-
Internal.setEpochNumberOfAction(token.key, confirmedUpdate.epoch, store)
|
|
143
|
-
} else if (Internal.isRootStore(store)) {
|
|
144
|
-
store.logger.info(
|
|
145
|
-
`❌`,
|
|
146
|
-
`transaction`,
|
|
147
|
-
token.key,
|
|
148
|
-
`received out-of-order update from server`,
|
|
149
|
-
{
|
|
150
|
-
clientEpoch: store.transactionMeta.epoch,
|
|
151
|
-
serverEpoch: confirmedUpdate.epoch,
|
|
152
|
-
},
|
|
153
|
-
)
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
socket.off(`tx-new:${token.key}`, registerAndAttemptConfirmedUpdate)
|
|
158
|
-
socket.on(`tx-new:${token.key}`, registerAndAttemptConfirmedUpdate)
|
|
159
|
-
socket.emit(`tx-sub:${token.key}`)
|
|
160
|
-
const unsubscribeFromIncomingUpdates = () => {
|
|
161
|
-
socket.off(`tx-new:${token.key}`, registerAndAttemptConfirmedUpdate)
|
|
162
|
-
socket.emit(`tx-unsub:${token.key}`)
|
|
163
|
-
}
|
|
164
|
-
return () => {
|
|
165
|
-
unsubscribeFromLocalUpdates()
|
|
166
|
-
unsubscribeFromIncomingUpdates()
|
|
167
|
-
}
|
|
168
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type * as AtomIO from "atom.io"
|
|
2
|
-
import { type Store, setIntoStore } from "atom.io/internal"
|
|
3
|
-
import type { Json } from "atom.io/json"
|
|
4
|
-
import type { Socket } from "socket.io-client"
|
|
5
|
-
|
|
6
|
-
export function syncState<J extends Json.Serializable>(
|
|
7
|
-
token: AtomIO.WritableToken<J>,
|
|
8
|
-
socket: Socket,
|
|
9
|
-
store: Store,
|
|
10
|
-
): () => void {
|
|
11
|
-
const setServedValue = (data: J) => {
|
|
12
|
-
setIntoStore(token, data, store)
|
|
13
|
-
}
|
|
14
|
-
socket.on(`value:${token.key}`, setServedValue)
|
|
15
|
-
socket.emit(`get:${token.key}`)
|
|
16
|
-
return () => {
|
|
17
|
-
socket.off(`value:${token.key}`, setServedValue)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type * as AtomIO from "atom.io"
|
|
2
|
-
import { actUponStore, arbitrary } from "atom.io/internal"
|
|
3
|
-
import { StoreContext } from "atom.io/react"
|
|
4
|
-
import * as RTC from "atom.io/realtime-client"
|
|
5
|
-
import * as React from "react"
|
|
6
|
-
|
|
7
|
-
import { useRealtimeService } from "./use-realtime-service"
|
|
8
|
-
|
|
9
|
-
export function useSyncAction<ƒ extends AtomIO.ƒn>(
|
|
10
|
-
token: AtomIO.TransactionToken<ƒ>,
|
|
11
|
-
): (...parameters: Parameters<ƒ>) => ReturnType<ƒ> {
|
|
12
|
-
const store = React.useContext(StoreContext)
|
|
13
|
-
useRealtimeService(`tx-sync:${token.key}`, (socket) => {
|
|
14
|
-
return RTC.syncAction(token, socket, store)
|
|
15
|
-
})
|
|
16
|
-
return actUponStore(token, arbitrary(), store)
|
|
17
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type * as AtomIO from "atom.io"
|
|
2
|
-
import type { Json } from "atom.io/json"
|
|
3
|
-
import { StoreContext, useO } from "atom.io/react"
|
|
4
|
-
import * as RTC from "atom.io/realtime-client"
|
|
5
|
-
import * as React from "react"
|
|
6
|
-
|
|
7
|
-
import { useRealtimeService } from "./use-realtime-service"
|
|
8
|
-
|
|
9
|
-
export function useSync<J extends Json.Serializable>(
|
|
10
|
-
token: AtomIO.WritableToken<J>,
|
|
11
|
-
): J {
|
|
12
|
-
const store = React.useContext(StoreContext)
|
|
13
|
-
useRealtimeService(`sync:${token.key}`, (socket) =>
|
|
14
|
-
RTC.syncState(token, socket, store),
|
|
15
|
-
)
|
|
16
|
-
return useO(token)
|
|
17
|
-
}
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import type { ChildProcessWithoutNullStreams } from "child_process"
|
|
2
|
-
|
|
3
|
-
import { Subject } from "atom.io/internal"
|
|
4
|
-
import { parseJson } from "atom.io/json"
|
|
5
|
-
import type { Json, Stringified } from "atom.io/json"
|
|
6
|
-
|
|
7
|
-
import type { Socket } from "."
|
|
8
|
-
|
|
9
|
-
export type Events = Json.Object<string, Json.Serializable[]>
|
|
10
|
-
|
|
11
|
-
export type StringifiedEvent<
|
|
12
|
-
Key extends string,
|
|
13
|
-
Params extends Json.Serializable[],
|
|
14
|
-
> = Stringified<[Key, ...Params]>
|
|
15
|
-
|
|
16
|
-
export interface EventBuffer<
|
|
17
|
-
Key extends string,
|
|
18
|
-
Params extends Json.Serializable[],
|
|
19
|
-
> extends Buffer {
|
|
20
|
-
toString(): StringifiedEvent<Key, Params>
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class CustomSocket<I extends Events, O extends Events> implements Socket {
|
|
24
|
-
protected listeners: Map<keyof I, Set<(...args: Json.Array) => void>>
|
|
25
|
-
protected globalListeners: Set<(event: string, ...args: Json.Array) => void>
|
|
26
|
-
protected handleEvent<Event extends keyof I>(
|
|
27
|
-
event: string,
|
|
28
|
-
...args: I[Event]
|
|
29
|
-
): void {
|
|
30
|
-
for (const listener of this.globalListeners) {
|
|
31
|
-
listener(event, ...args)
|
|
32
|
-
}
|
|
33
|
-
const listeners = this.listeners.get(event)
|
|
34
|
-
if (listeners) {
|
|
35
|
-
for (const listener of listeners) {
|
|
36
|
-
listener(...args)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
public id = `no_id_retrieved`
|
|
42
|
-
|
|
43
|
-
public constructor(
|
|
44
|
-
public emit: <Event extends keyof O>(
|
|
45
|
-
event: Event,
|
|
46
|
-
...args: O[Event]
|
|
47
|
-
) => CustomSocket<I, O>,
|
|
48
|
-
) {
|
|
49
|
-
this.listeners = new Map()
|
|
50
|
-
this.globalListeners = new Set()
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
public on<Event extends keyof I>(
|
|
54
|
-
event: Event,
|
|
55
|
-
listener: (...args: I[Event]) => void,
|
|
56
|
-
): CustomSocket<I, O> {
|
|
57
|
-
const listeners = this.listeners.get(event)
|
|
58
|
-
if (listeners) {
|
|
59
|
-
listeners.add(listener)
|
|
60
|
-
} else {
|
|
61
|
-
this.listeners.set(event, new Set([listener]))
|
|
62
|
-
}
|
|
63
|
-
return this
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
public onAny(
|
|
67
|
-
listener: (event: string, ...args: Json.Array) => void,
|
|
68
|
-
): CustomSocket<I, O> {
|
|
69
|
-
this.globalListeners.add(listener)
|
|
70
|
-
return this
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
public off<Event extends keyof I>(
|
|
74
|
-
event: Event,
|
|
75
|
-
listener: (...args: I[Event]) => void,
|
|
76
|
-
): CustomSocket<I, O> {
|
|
77
|
-
const listeners = this.listeners.get(event)
|
|
78
|
-
if (listeners) {
|
|
79
|
-
listeners.delete(listener)
|
|
80
|
-
}
|
|
81
|
-
return this
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
public offAny(
|
|
85
|
-
listener: (event: string, ...args: Json.Array) => void,
|
|
86
|
-
): CustomSocket<I, O> {
|
|
87
|
-
this.globalListeners.delete(listener)
|
|
88
|
-
return this
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export class ChildSocket<
|
|
93
|
-
I extends Events,
|
|
94
|
-
O extends Events & {
|
|
95
|
-
/* eslint-disable quotes */
|
|
96
|
-
"setup-relay": [string]
|
|
97
|
-
/* eslint-enable quotes */
|
|
98
|
-
},
|
|
99
|
-
> extends CustomSocket<I, O> {
|
|
100
|
-
protected process: ChildProcessWithoutNullStreams
|
|
101
|
-
|
|
102
|
-
public id = `no_id_retrieved`
|
|
103
|
-
|
|
104
|
-
public constructor(process: ChildProcessWithoutNullStreams) {
|
|
105
|
-
super((event, ...args) => {
|
|
106
|
-
const stringifiedEvent = JSON.stringify([event, ...args]) + `\n`
|
|
107
|
-
this.process.stdin.write(stringifiedEvent)
|
|
108
|
-
return this
|
|
109
|
-
})
|
|
110
|
-
this.process = process
|
|
111
|
-
this.process.stdout.on(
|
|
112
|
-
`data`,
|
|
113
|
-
<Event extends keyof I>(buffer: EventBuffer<string, I[Event]>) => {
|
|
114
|
-
const stringifiedEvent = buffer.toString()
|
|
115
|
-
const parsedEvent = parseJson(stringifiedEvent)
|
|
116
|
-
this.handleEvent(...parsedEvent)
|
|
117
|
-
},
|
|
118
|
-
)
|
|
119
|
-
if (process.pid) {
|
|
120
|
-
this.id = process.pid.toString()
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export class SubjectSocket<
|
|
126
|
-
I extends Events,
|
|
127
|
-
O extends Events,
|
|
128
|
-
> extends CustomSocket<I, O> {
|
|
129
|
-
public in: Subject<[string, ...Json.Serializable[]]>
|
|
130
|
-
public out: Subject<[string, ...Json.Serializable[]]>
|
|
131
|
-
public id = `no_id_retrieved`
|
|
132
|
-
|
|
133
|
-
public constructor(id: string) {
|
|
134
|
-
super((...args) => {
|
|
135
|
-
this.out.next(args as any)
|
|
136
|
-
return this
|
|
137
|
-
})
|
|
138
|
-
this.id = id
|
|
139
|
-
this.in = new Subject()
|
|
140
|
-
this.out = new Subject()
|
|
141
|
-
this.in.subscribe(`socket`, (event) => {
|
|
142
|
-
this.handleEvent(...(event as [string, ...I[keyof I]]))
|
|
143
|
-
})
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export class ParentSocket<
|
|
148
|
-
I extends Events & {
|
|
149
|
-
[id in string as `relay:${id}`]: [string, ...Json.Serializable[]]
|
|
150
|
-
} & {
|
|
151
|
-
/* eslint-disable quotes */
|
|
152
|
-
"setup-relay": [string]
|
|
153
|
-
/* eslint-enable quotes */
|
|
154
|
-
},
|
|
155
|
-
O extends Events & {
|
|
156
|
-
[id in string as `relay:${id}`]: [string, ...Json.Serializable[]]
|
|
157
|
-
},
|
|
158
|
-
> extends CustomSocket<I, O> {
|
|
159
|
-
protected queue: string[]
|
|
160
|
-
protected relays: Map<string, SubjectSocket<any, any>>
|
|
161
|
-
protected relayServices: ((
|
|
162
|
-
socket: SubjectSocket<any, any>,
|
|
163
|
-
) => (() => void) | void)[]
|
|
164
|
-
protected process: NodeJS.Process
|
|
165
|
-
|
|
166
|
-
public id = `no_id_retrieved`
|
|
167
|
-
|
|
168
|
-
public constructor() {
|
|
169
|
-
super((event, ...args) => {
|
|
170
|
-
const stringifiedEvent = JSON.stringify([event, ...args])
|
|
171
|
-
this.process.stdout.write(stringifiedEvent)
|
|
172
|
-
return this
|
|
173
|
-
})
|
|
174
|
-
this.process = process
|
|
175
|
-
this.process.stdin.resume()
|
|
176
|
-
this.queue = []
|
|
177
|
-
this.relays = new Map()
|
|
178
|
-
this.relayServices = []
|
|
179
|
-
|
|
180
|
-
this.process.stdin.on(
|
|
181
|
-
`data`,
|
|
182
|
-
<Event extends keyof I>(chunk: EventBuffer<string, I[Event]>) => {
|
|
183
|
-
const buffer = chunk.toString()
|
|
184
|
-
this.queue.push(...buffer.split(`\n`))
|
|
185
|
-
|
|
186
|
-
while (this.queue.length > 0) {
|
|
187
|
-
try {
|
|
188
|
-
const event = this.queue.shift() as StringifiedEvent<any, any>
|
|
189
|
-
if (event === ``) continue
|
|
190
|
-
const parsedEvent = parseJson(event)
|
|
191
|
-
this.handleEvent(...(parsedEvent as [string, ...I[keyof I]]))
|
|
192
|
-
} catch (error) {
|
|
193
|
-
this.process.stderr.write(`❌ ${error}\n`)
|
|
194
|
-
break
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
},
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
// process.stdin.on(`end`, () => process.exit(0))
|
|
201
|
-
process.on(`SIGINT`, () => process.exit(0))
|
|
202
|
-
if (process.pid) {
|
|
203
|
-
this.id = process.pid?.toString()
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
this.on(`setup-relay`, (id: string) => {
|
|
207
|
-
const relay = new SubjectSocket(`relay:${id}`)
|
|
208
|
-
this.relays.set(id, relay)
|
|
209
|
-
for (const attachServices of this.relayServices) {
|
|
210
|
-
attachServices(relay)
|
|
211
|
-
}
|
|
212
|
-
this.on(`relay:${id}`, (...data) => {
|
|
213
|
-
relay.in.next(data)
|
|
214
|
-
})
|
|
215
|
-
relay.out.subscribe(`socket`, (data) => {
|
|
216
|
-
this.emit(...(data as [string, ...O[keyof O]]))
|
|
217
|
-
})
|
|
218
|
-
})
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
public relay(
|
|
222
|
-
attachServices: (socket: SubjectSocket<any, any>) => (() => void) | void,
|
|
223
|
-
): void {
|
|
224
|
-
this.relayServices.push(attachServices)
|
|
225
|
-
const relays = this.relays.values()
|
|
226
|
-
for (const relay of relays) {
|
|
227
|
-
attachServices(relay)
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import type * as AtomIO from "atom.io"
|
|
2
|
-
import {
|
|
3
|
-
IMPLICIT,
|
|
4
|
-
actUponStore,
|
|
5
|
-
assignTransactionToContinuity,
|
|
6
|
-
findInStore,
|
|
7
|
-
getFromStore,
|
|
8
|
-
setIntoStore,
|
|
9
|
-
subscribeToTransaction,
|
|
10
|
-
} from "atom.io/internal"
|
|
11
|
-
import type { Json, JsonIO } from "atom.io/json"
|
|
12
|
-
|
|
13
|
-
import type { ServerConfig } from "."
|
|
14
|
-
import {
|
|
15
|
-
completeUpdateAtoms,
|
|
16
|
-
redactedUpdateSelectors,
|
|
17
|
-
transactionRedactorAtoms,
|
|
18
|
-
userUnacknowledgedQueues,
|
|
19
|
-
usersOfSockets,
|
|
20
|
-
} from "./realtime-server-stores"
|
|
21
|
-
|
|
22
|
-
export type ActionSynchronizer = ReturnType<typeof realtimeActionSynchronizer>
|
|
23
|
-
export function realtimeActionSynchronizer({
|
|
24
|
-
socket,
|
|
25
|
-
store = IMPLICIT.STORE,
|
|
26
|
-
}: ServerConfig) {
|
|
27
|
-
return function actionSynchronizer<ƒ extends JsonIO>(
|
|
28
|
-
tx: AtomIO.TransactionToken<ƒ>,
|
|
29
|
-
filter?: (
|
|
30
|
-
update: AtomIO.TransactionUpdateContent[],
|
|
31
|
-
) => AtomIO.TransactionUpdateContent[],
|
|
32
|
-
): () => void {
|
|
33
|
-
assignTransactionToContinuity(`default`, tx.key, store)
|
|
34
|
-
|
|
35
|
-
const userKeyState = findInStore(
|
|
36
|
-
usersOfSockets.states.userKeyOfSocket,
|
|
37
|
-
socket.id,
|
|
38
|
-
store,
|
|
39
|
-
)
|
|
40
|
-
const userKey = getFromStore(userKeyState, store)
|
|
41
|
-
if (!userKey) {
|
|
42
|
-
store.logger.error(
|
|
43
|
-
`❌`,
|
|
44
|
-
`transaction`,
|
|
45
|
-
tx.key,
|
|
46
|
-
`Tried to create a synchronizer for a socket (${socket.id}) that is not connected to a user.`,
|
|
47
|
-
)
|
|
48
|
-
return () => {}
|
|
49
|
-
}
|
|
50
|
-
const userUnacknowledgedQueue = findInStore(
|
|
51
|
-
userUnacknowledgedQueues,
|
|
52
|
-
userKey,
|
|
53
|
-
store,
|
|
54
|
-
)
|
|
55
|
-
const userUnacknowledgedUpdates = getFromStore(
|
|
56
|
-
userUnacknowledgedQueue,
|
|
57
|
-
store,
|
|
58
|
-
)
|
|
59
|
-
if (filter) {
|
|
60
|
-
const redactorState = findInStore(transactionRedactorAtoms, tx.key, store)
|
|
61
|
-
setIntoStore(redactorState, { filter }, store)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const fillTransactionRequest = (
|
|
65
|
-
update: Pick<AtomIO.TransactionUpdate<ƒ>, `id` | `params`>,
|
|
66
|
-
) => {
|
|
67
|
-
const performanceKey = `tx-run:${tx.key}:${update.id}`
|
|
68
|
-
const performanceKeyStart = `${performanceKey}:start`
|
|
69
|
-
const performanceKeyEnd = `${performanceKey}:end`
|
|
70
|
-
performance.mark(performanceKeyStart)
|
|
71
|
-
actUponStore<ƒ>(tx, update.id, store)(...update.params)
|
|
72
|
-
performance.mark(performanceKeyEnd)
|
|
73
|
-
const metric = performance.measure(
|
|
74
|
-
performanceKey,
|
|
75
|
-
performanceKeyStart,
|
|
76
|
-
performanceKeyEnd,
|
|
77
|
-
)
|
|
78
|
-
store?.logger.info(`🚀`, `transaction`, tx.key, update.id, metric.duration)
|
|
79
|
-
}
|
|
80
|
-
socket.off(`tx-run:${tx.key}`, fillTransactionRequest)
|
|
81
|
-
socket.on(`tx-run:${tx.key}`, fillTransactionRequest)
|
|
82
|
-
|
|
83
|
-
let unsubscribeFromTransaction: (() => void) | undefined
|
|
84
|
-
const fillTransactionSubscriptionRequest = () => {
|
|
85
|
-
unsubscribeFromTransaction = subscribeToTransaction(
|
|
86
|
-
tx,
|
|
87
|
-
(update) => {
|
|
88
|
-
const updateState = findInStore(completeUpdateAtoms, update.id, store)
|
|
89
|
-
setIntoStore(updateState, update, store)
|
|
90
|
-
const toEmit: Pick<
|
|
91
|
-
AtomIO.TransactionUpdate<ƒ>,
|
|
92
|
-
`epoch` | `id` | `key` | `output` | `updates`
|
|
93
|
-
> | null = filter
|
|
94
|
-
? getFromStore(
|
|
95
|
-
findInStore(redactedUpdateSelectors, [tx.key, update.id], store),
|
|
96
|
-
store,
|
|
97
|
-
)
|
|
98
|
-
: update
|
|
99
|
-
|
|
100
|
-
// the problem is that only while a socket is connected can
|
|
101
|
-
// updates be set in the queue for that socket's client.
|
|
102
|
-
//
|
|
103
|
-
// we need a client session that can persist between disconnects
|
|
104
|
-
setIntoStore(
|
|
105
|
-
userUnacknowledgedQueue,
|
|
106
|
-
(updates) => {
|
|
107
|
-
if (toEmit) {
|
|
108
|
-
updates.push(toEmit)
|
|
109
|
-
updates.sort((a, b) => a.epoch - b.epoch)
|
|
110
|
-
}
|
|
111
|
-
return updates
|
|
112
|
-
},
|
|
113
|
-
store,
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
socket.emit(`tx-new:${tx.key}`, toEmit as Json.Serializable)
|
|
117
|
-
},
|
|
118
|
-
`tx-sub:${tx.key}:${socket.id}`,
|
|
119
|
-
store,
|
|
120
|
-
)
|
|
121
|
-
socket.on(`tx-unsub:${tx.key}`, unsubscribeFromTransaction)
|
|
122
|
-
}
|
|
123
|
-
socket.on(`tx-sub:${tx.key}`, fillTransactionSubscriptionRequest)
|
|
124
|
-
|
|
125
|
-
let i = 1
|
|
126
|
-
let next = 1
|
|
127
|
-
const retry = setInterval(() => {
|
|
128
|
-
const toEmit = userUnacknowledgedUpdates[0]
|
|
129
|
-
if (toEmit && i === next) {
|
|
130
|
-
socket.emit(`tx-new:${tx.key}`, toEmit as Json.Serializable)
|
|
131
|
-
next *= 2
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
i++
|
|
135
|
-
}, 250)
|
|
136
|
-
|
|
137
|
-
const trackClientAcknowledgement = (epoch: number) => {
|
|
138
|
-
i = 1
|
|
139
|
-
next = 1
|
|
140
|
-
8
|
|
141
|
-
if (userUnacknowledgedUpdates[0]?.epoch === epoch) {
|
|
142
|
-
setIntoStore(
|
|
143
|
-
userUnacknowledgedQueue,
|
|
144
|
-
(updates) => {
|
|
145
|
-
updates.shift()
|
|
146
|
-
return updates
|
|
147
|
-
},
|
|
148
|
-
store,
|
|
149
|
-
)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
socket.on(`tx-ack:${tx.key}`, trackClientAcknowledgement)
|
|
153
|
-
|
|
154
|
-
return () => {
|
|
155
|
-
if (unsubscribeFromTransaction) {
|
|
156
|
-
unsubscribeFromTransaction()
|
|
157
|
-
unsubscribeFromTransaction = undefined
|
|
158
|
-
}
|
|
159
|
-
clearInterval(retry)
|
|
160
|
-
socket.off(`tx-run:${tx.key}`, fillTransactionRequest)
|
|
161
|
-
socket.off(`tx-sub:${tx.key}`, fillTransactionSubscriptionRequest)
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import type { ChildProcessWithoutNullStreams } from "child_process"
|
|
2
|
-
import { spawn } from "child_process"
|
|
3
|
-
|
|
4
|
-
import * as AtomIO from "atom.io"
|
|
5
|
-
import type { Loadable } from "atom.io/data"
|
|
6
|
-
import { join } from "atom.io/data"
|
|
7
|
-
import { SetRTX } from "atom.io/transceivers/set-rtx"
|
|
8
|
-
|
|
9
|
-
export type RoomArguments =
|
|
10
|
-
| [script: string, options: string[]]
|
|
11
|
-
| [script: string]
|
|
12
|
-
|
|
13
|
-
export const roomIndex = AtomIO.atom({
|
|
14
|
-
key: `roomIndex`,
|
|
15
|
-
default: () => new SetRTX<string>(),
|
|
16
|
-
mutable: true,
|
|
17
|
-
toJson: (set) => set.toJSON(),
|
|
18
|
-
fromJson: (json) => SetRTX.fromJSON(json),
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
export type UserInRoomMeta = {
|
|
22
|
-
enteredAtEpoch: number
|
|
23
|
-
}
|
|
24
|
-
export const DEFAULT_USER_IN_ROOM_META: UserInRoomMeta = {
|
|
25
|
-
enteredAtEpoch: 0,
|
|
26
|
-
}
|
|
27
|
-
export const usersInRooms = join(
|
|
28
|
-
{
|
|
29
|
-
key: `usersInRooms`,
|
|
30
|
-
between: [`room`, `user`],
|
|
31
|
-
cardinality: `1:n`,
|
|
32
|
-
},
|
|
33
|
-
DEFAULT_USER_IN_ROOM_META,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
export const roomArgumentsAtoms = AtomIO.atomFamily<RoomArguments, string>({
|
|
37
|
-
key: `roomArguments`,
|
|
38
|
-
default: [`echo Hello World!`],
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
export const roomSelectors = AtomIO.selectorFamily<
|
|
42
|
-
Loadable<ChildProcessWithoutNullStreams>,
|
|
43
|
-
string
|
|
44
|
-
>({
|
|
45
|
-
key: `room`,
|
|
46
|
-
get:
|
|
47
|
-
(roomId) =>
|
|
48
|
-
({ get, find }) => {
|
|
49
|
-
const argumentsState = find(roomArgumentsAtoms, roomId)
|
|
50
|
-
const args = get(argumentsState)
|
|
51
|
-
const [script, options] = args
|
|
52
|
-
return new Promise((resolve) => {
|
|
53
|
-
const room = spawn(script, options, { env: process.env })
|
|
54
|
-
const resolver = (data: Buffer) => {
|
|
55
|
-
if (data.toString() === `✨`) {
|
|
56
|
-
room.stdout.off(`data`, resolver)
|
|
57
|
-
resolve(room)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
room.stdout.on(`data`, resolver)
|
|
61
|
-
})
|
|
62
|
-
},
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
export const createRoomTX = AtomIO.transaction<
|
|
66
|
-
(
|
|
67
|
-
roomId: string,
|
|
68
|
-
script: string,
|
|
69
|
-
options?: string[],
|
|
70
|
-
) => Loadable<ChildProcessWithoutNullStreams>
|
|
71
|
-
>({
|
|
72
|
-
key: `createRoom`,
|
|
73
|
-
do: ({ get, set, find }, roomId, script, options) => {
|
|
74
|
-
const args: RoomArguments = options ? [script, options] : [script]
|
|
75
|
-
const roomArgumentsState = find(roomArgumentsAtoms, roomId)
|
|
76
|
-
set(roomArgumentsState, args)
|
|
77
|
-
set(roomIndex, (s) => s.add(roomId))
|
|
78
|
-
const roomState = find(roomSelectors, roomId)
|
|
79
|
-
const room = get(roomState)
|
|
80
|
-
return room
|
|
81
|
-
},
|
|
82
|
-
})
|
|
83
|
-
export type CreateRoomIO = AtomIO.TransactionIO<typeof createRoomTX>
|
|
84
|
-
|
|
85
|
-
export const joinRoomTX = AtomIO.transaction<
|
|
86
|
-
(roomId: string, userId: string, enteredAtEpoch: number) => UserInRoomMeta
|
|
87
|
-
>({
|
|
88
|
-
key: `joinRoom`,
|
|
89
|
-
do: (transactors, roomId, userId, enteredAtEpoch) => {
|
|
90
|
-
const meta = { enteredAtEpoch }
|
|
91
|
-
usersInRooms.transact(transactors, ({ relations }) => {
|
|
92
|
-
relations.set(roomId, userId, meta)
|
|
93
|
-
})
|
|
94
|
-
return meta
|
|
95
|
-
},
|
|
96
|
-
})
|
|
97
|
-
export type JoinRoomIO = AtomIO.TransactionIO<typeof joinRoomTX>
|