@pluv/io 0.0.0
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/.eslintrc.js +4 -0
- package/.turbo/turbo-build.log +17 -0
- package/dist/index.d.ts +257 -0
- package/dist/index.js +891 -0
- package/dist/index.mjs +857 -0
- package/package.json +34 -0
- package/src/AbstractPersistance.ts +31 -0
- package/src/AbstractPlatform.ts +40 -0
- package/src/AbstractPubSub.ts +23 -0
- package/src/AbstractWebSocket.ts +117 -0
- package/src/IORoom.ts +630 -0
- package/src/Persistance.ts +77 -0
- package/src/PluvIO.ts +322 -0
- package/src/PubSub.ts +79 -0
- package/src/authorize.ts +81 -0
- package/src/createIO.ts +51 -0
- package/src/index.ts +34 -0
- package/src/types.ts +93 -0
- package/tsconfig.json +5 -0
package/src/PluvIO.ts
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EventRecord,
|
|
3
|
+
InferIOAuthorize,
|
|
4
|
+
InferIOAuthorizeUser,
|
|
5
|
+
InferIOInput,
|
|
6
|
+
InferIOOutput,
|
|
7
|
+
IOAuthorize,
|
|
8
|
+
IOLike,
|
|
9
|
+
JsonObject,
|
|
10
|
+
Spread,
|
|
11
|
+
BaseIOAuthorize,
|
|
12
|
+
} from "@pluv/types";
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
import type { AbstractPlatform } from "./AbstractPlatform";
|
|
15
|
+
import type { JWTEncodeParams } from "./authorize";
|
|
16
|
+
import { authorize } from "./authorize";
|
|
17
|
+
import { IORoom } from "./IORoom";
|
|
18
|
+
import type { EventConfig, InferEventConfig } from "./types";
|
|
19
|
+
|
|
20
|
+
export type InferIOContext<TIO extends PluvIO<any, any, any, any, any>> =
|
|
21
|
+
TIO extends PluvIO<any, any, infer IContext, any, any> ? IContext : never;
|
|
22
|
+
|
|
23
|
+
export type InferIOPlatform<TIO extends PluvIO<any, any, any, any>> =
|
|
24
|
+
TIO extends PluvIO<infer IPlatform, any, any, any, any> ? IPlatform : never;
|
|
25
|
+
|
|
26
|
+
export type InferIORoom<TIO extends PluvIO<any, any, any, any>> =
|
|
27
|
+
TIO extends PluvIO<
|
|
28
|
+
infer IPlatform,
|
|
29
|
+
infer IAuthorize,
|
|
30
|
+
infer IContext,
|
|
31
|
+
infer IInput,
|
|
32
|
+
infer IOutput
|
|
33
|
+
>
|
|
34
|
+
? IORoom<IPlatform, IAuthorize, IContext, IInput, IOutput>
|
|
35
|
+
: never;
|
|
36
|
+
|
|
37
|
+
export interface PluvIORegisterOptions {
|
|
38
|
+
token?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PluvIOConfig<
|
|
42
|
+
TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
|
|
43
|
+
TAuthorize extends IOAuthorize<any, any> | null = null,
|
|
44
|
+
TContext extends JsonObject = {},
|
|
45
|
+
TInput extends EventRecord<string, any> = {},
|
|
46
|
+
TOutput extends EventRecord<string, any> = {}
|
|
47
|
+
> {
|
|
48
|
+
authorize?: TAuthorize;
|
|
49
|
+
context?: TContext;
|
|
50
|
+
debug?: boolean;
|
|
51
|
+
events?: InferEventConfig<TContext, TInput, TOutput>;
|
|
52
|
+
platform: TPlatform;
|
|
53
|
+
room?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class PluvIO<
|
|
57
|
+
TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
|
|
58
|
+
TAuthorize extends IOAuthorize<any, any> = BaseIOAuthorize,
|
|
59
|
+
TContext extends JsonObject = {},
|
|
60
|
+
TInput extends EventRecord<string, any> = {},
|
|
61
|
+
TOutput extends EventRecord<string, any> = {}
|
|
62
|
+
> implements IOLike<TAuthorize, TInput, TOutput>
|
|
63
|
+
{
|
|
64
|
+
readonly _authorize: TAuthorize | null = null;
|
|
65
|
+
readonly _debug: boolean;
|
|
66
|
+
readonly _events: InferEventConfig<TContext, TInput, TOutput> = {
|
|
67
|
+
$PING: {
|
|
68
|
+
resolver: (data, { session }) => {
|
|
69
|
+
if (!session) return {};
|
|
70
|
+
|
|
71
|
+
const currentTime = new Date().getTime();
|
|
72
|
+
|
|
73
|
+
session.timers.ping = currentTime;
|
|
74
|
+
|
|
75
|
+
return { $PONG: {} };
|
|
76
|
+
},
|
|
77
|
+
options: { type: "self" },
|
|
78
|
+
},
|
|
79
|
+
$GET_OTHERS: {
|
|
80
|
+
resolver: (data, { session, sessions }) => {
|
|
81
|
+
const others = Array.from(sessions.entries())
|
|
82
|
+
.filter(([, wsSession]) => wsSession.id !== session?.id)
|
|
83
|
+
.reduce<{
|
|
84
|
+
[connectionId: string]: {
|
|
85
|
+
connectionId: string;
|
|
86
|
+
room: string | null;
|
|
87
|
+
user: InferIOAuthorizeUser<TAuthorize>;
|
|
88
|
+
};
|
|
89
|
+
}>(
|
|
90
|
+
(acc, [connectionId, { presence, user }]) => ({
|
|
91
|
+
...acc,
|
|
92
|
+
[connectionId]: {
|
|
93
|
+
connectionId,
|
|
94
|
+
presence,
|
|
95
|
+
room: this.room,
|
|
96
|
+
user: user as InferIOAuthorizeUser<TAuthorize>,
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
{}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return { $OTHERS_RECEIVED: { others } };
|
|
103
|
+
},
|
|
104
|
+
options: { type: "sync" },
|
|
105
|
+
},
|
|
106
|
+
$UPDATE_PRESENCE: {
|
|
107
|
+
resolver: ({ presence }, { session }) => {
|
|
108
|
+
if (!session) return {};
|
|
109
|
+
|
|
110
|
+
session.presence = Object.assign(
|
|
111
|
+
Object.create(null),
|
|
112
|
+
session.presence,
|
|
113
|
+
presence
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return { $PRESENCE_UPDATED: { presence } };
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
$UPDATE_STORAGE: {
|
|
120
|
+
resolver: ({ origin, update }, { doc, room }) => {
|
|
121
|
+
if (
|
|
122
|
+
origin === "$STORAGE_RECEIVED" &&
|
|
123
|
+
Object.keys(doc.toJSON()).length
|
|
124
|
+
) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const updated = doc.applyUpdate(update);
|
|
129
|
+
const state = updated.encodeStateAsUpdate();
|
|
130
|
+
|
|
131
|
+
this._platform.persistance.setStorageState(room, state);
|
|
132
|
+
|
|
133
|
+
return { $STORAGE_UPDATED: { state } };
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
} as InferEventConfig<TContext, TInput, TOutput>;
|
|
137
|
+
readonly _platform: TPlatform;
|
|
138
|
+
|
|
139
|
+
readonly room: string | null = null;
|
|
140
|
+
|
|
141
|
+
private _context: TContext = {} as TContext;
|
|
142
|
+
private _rooms = new Map<
|
|
143
|
+
string,
|
|
144
|
+
IORoom<TPlatform, TAuthorize, TContext, TInput, TOutput>
|
|
145
|
+
>();
|
|
146
|
+
|
|
147
|
+
constructor(
|
|
148
|
+
options: PluvIOConfig<TPlatform, TAuthorize, TContext, TInput, TOutput>
|
|
149
|
+
) {
|
|
150
|
+
const {
|
|
151
|
+
authorize,
|
|
152
|
+
context,
|
|
153
|
+
debug = false,
|
|
154
|
+
events,
|
|
155
|
+
platform,
|
|
156
|
+
room,
|
|
157
|
+
} = options;
|
|
158
|
+
|
|
159
|
+
this._debug = debug;
|
|
160
|
+
this._platform = platform;
|
|
161
|
+
|
|
162
|
+
if (authorize) this._authorize = authorize;
|
|
163
|
+
if (context) this._context = context;
|
|
164
|
+
if (events) this._events = events;
|
|
165
|
+
if (room) this.room = room;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public async createToken(
|
|
169
|
+
params: JWTEncodeParams<InferIOAuthorizeUser<InferIOAuthorize<this>>>
|
|
170
|
+
): Promise<string> {
|
|
171
|
+
if (!this._authorize) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
"IO does not specify authorize during initialization."
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return await authorize({
|
|
178
|
+
platform: this._platform,
|
|
179
|
+
secret: this._authorize.secret,
|
|
180
|
+
}).encode(params);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public event<
|
|
184
|
+
TEvent extends string,
|
|
185
|
+
TData extends JsonObject = {},
|
|
186
|
+
TResult extends EventRecord<string, any> = {}
|
|
187
|
+
>(
|
|
188
|
+
event: TEvent,
|
|
189
|
+
config: EventConfig<TContext, TData, TResult>
|
|
190
|
+
): PluvIO<
|
|
191
|
+
TPlatform,
|
|
192
|
+
TAuthorize,
|
|
193
|
+
TContext,
|
|
194
|
+
Spread<[TInput, EventRecord<TEvent, TData>]>,
|
|
195
|
+
Spread<[TOutput, TResult]>
|
|
196
|
+
> {
|
|
197
|
+
if (event.startsWith("$")) {
|
|
198
|
+
throw new Error('Event name must not start with "$".');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const newEvent = {
|
|
202
|
+
[event]: config,
|
|
203
|
+
} as InferEventConfig<
|
|
204
|
+
TContext,
|
|
205
|
+
EventRecord<TEvent, TData>,
|
|
206
|
+
TResult extends EventRecord<string, any> ? TResult : {}
|
|
207
|
+
>;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* !HACK
|
|
211
|
+
* @description Typing this is becoming way too unmanagable, due to resolver
|
|
212
|
+
* being able to return void and needing to handle the conditional typing.
|
|
213
|
+
* Assume the types will work out, and cast as any here.
|
|
214
|
+
*
|
|
215
|
+
* TODO
|
|
216
|
+
* @description Revisit EventResolver and related types to resolve types
|
|
217
|
+
* without casting to any.
|
|
218
|
+
*
|
|
219
|
+
* @author leedavidcs
|
|
220
|
+
* @date September 25, 2022
|
|
221
|
+
*/
|
|
222
|
+
return PluvIO.merge(
|
|
223
|
+
this as any,
|
|
224
|
+
new PluvIO({
|
|
225
|
+
events: newEvent as any,
|
|
226
|
+
platform: this._platform,
|
|
227
|
+
})
|
|
228
|
+
) as any;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public getRoom(
|
|
232
|
+
room: string
|
|
233
|
+
): IORoom<TPlatform, TAuthorize, TContext, TInput, TOutput> {
|
|
234
|
+
if (this.room) {
|
|
235
|
+
throw new Error("IO cannot getRoom from within a room.");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!/^[a-z0-9]+[a-z0-9\-_]+[a-z0-9]+$/i.test(room)) {
|
|
239
|
+
throw new Error("Unsupported room name");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const platform = this._platform;
|
|
243
|
+
const oldRoom = this._rooms.get(room);
|
|
244
|
+
|
|
245
|
+
if (oldRoom) return oldRoom;
|
|
246
|
+
|
|
247
|
+
const newRoom = new IORoom<
|
|
248
|
+
TPlatform,
|
|
249
|
+
TAuthorize,
|
|
250
|
+
TContext,
|
|
251
|
+
TInput,
|
|
252
|
+
TOutput
|
|
253
|
+
>(room, {
|
|
254
|
+
authorize: this._authorize ?? undefined,
|
|
255
|
+
context: this._context,
|
|
256
|
+
debug: this._debug,
|
|
257
|
+
events: this._events,
|
|
258
|
+
onDestroy: () => {
|
|
259
|
+
this._logDebug(`${chalk.blue("Deleting empty room:")} ${room}`);
|
|
260
|
+
|
|
261
|
+
this._rooms.delete(room);
|
|
262
|
+
|
|
263
|
+
if (this._debug) {
|
|
264
|
+
const rooms = Array.from(this._rooms.keys());
|
|
265
|
+
|
|
266
|
+
this._logDebug(`${chalk.blue("Deleted room:")} ${room}`);
|
|
267
|
+
this._logDebug(
|
|
268
|
+
`${chalk.blue("Rooms available:")} ${rooms.join(", ")}`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
platform,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
this._rooms.set(room, newRoom);
|
|
276
|
+
|
|
277
|
+
if (this._debug) {
|
|
278
|
+
const rooms = Array.from(this._rooms.keys());
|
|
279
|
+
|
|
280
|
+
this._logDebug(`${chalk.blue("Created room:")} ${room}`);
|
|
281
|
+
this._logDebug(
|
|
282
|
+
`${chalk.blue("Rooms available:")} ${rooms.join(", ")}`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return newRoom;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
public static merge<TIO1 extends PluvIO, TIO2 extends PluvIO>(
|
|
290
|
+
first: TIO1,
|
|
291
|
+
second: TIO2
|
|
292
|
+
): PluvIO<
|
|
293
|
+
InferIOPlatform<TIO1>,
|
|
294
|
+
InferIOAuthorize<TIO1>,
|
|
295
|
+
InferIOContext<TIO1>,
|
|
296
|
+
Spread<[InferIOInput<TIO1>, InferIOInput<TIO2>]>,
|
|
297
|
+
Spread<[InferIOOutput<TIO1>, InferIOOutput<TIO2>]>
|
|
298
|
+
> {
|
|
299
|
+
const authorize = (first._authorize ??
|
|
300
|
+
undefined) as InferIOAuthorize<TIO1>;
|
|
301
|
+
const debug = first._debug;
|
|
302
|
+
const platform = first._platform as InferIOPlatform<TIO1>;
|
|
303
|
+
const room = first.room ?? undefined;
|
|
304
|
+
|
|
305
|
+
const events1 = first._events;
|
|
306
|
+
const events2 = second._events;
|
|
307
|
+
|
|
308
|
+
const events = Object.assign(Object.create(null), events1, events2);
|
|
309
|
+
|
|
310
|
+
return new PluvIO({
|
|
311
|
+
authorize,
|
|
312
|
+
debug,
|
|
313
|
+
events,
|
|
314
|
+
platform,
|
|
315
|
+
room,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private _logDebug(...data: any[]): void {
|
|
320
|
+
this._debug && console.log(...data);
|
|
321
|
+
}
|
|
322
|
+
}
|
package/src/PubSub.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Subject } from "wonka";
|
|
2
|
+
import { makeSubject, subscribe } from "wonka";
|
|
3
|
+
import type { IOPubSubEventMessage } from "./AbstractPubSub";
|
|
4
|
+
import { AbstractPubSub } from "./AbstractPubSub";
|
|
5
|
+
|
|
6
|
+
export class PubSub extends AbstractPubSub {
|
|
7
|
+
private _currentSubscriptionId: number = 0;
|
|
8
|
+
// Map of roomName to subjects
|
|
9
|
+
private _subjects = new Map<string, Subject<IOPubSubEventMessage<any>>>();
|
|
10
|
+
// Map of roomName to subscription ids
|
|
11
|
+
private _subscriptionsRefs = new Map<string, readonly number[]>();
|
|
12
|
+
// Map of
|
|
13
|
+
private _unsubscriptions = new Map<
|
|
14
|
+
number,
|
|
15
|
+
[roomName: string, unsubscribe: () => void]
|
|
16
|
+
>();
|
|
17
|
+
|
|
18
|
+
public async publish(
|
|
19
|
+
roomName: string,
|
|
20
|
+
payload: IOPubSubEventMessage<any>
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
const subject = this._subjects.get(roomName);
|
|
23
|
+
|
|
24
|
+
if (!subject) return;
|
|
25
|
+
|
|
26
|
+
Promise.resolve(subject.next(payload));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public subscribe(
|
|
30
|
+
roomName: string,
|
|
31
|
+
onMessage: (message: IOPubSubEventMessage<any>) => void
|
|
32
|
+
): Promise<number> {
|
|
33
|
+
const id = this._currentSubscriptionId++;
|
|
34
|
+
const refs = this._subscriptionsRefs.get(roomName) ?? [];
|
|
35
|
+
|
|
36
|
+
const subject = makeSubject<IOPubSubEventMessage<any>>();
|
|
37
|
+
|
|
38
|
+
this._subjects.set(roomName, subject);
|
|
39
|
+
this._subscriptionsRefs.set(roomName, [...refs, id]);
|
|
40
|
+
|
|
41
|
+
const unsubscribe = subscribe(onMessage)(subject.source).unsubscribe;
|
|
42
|
+
|
|
43
|
+
this._unsubscriptions.set(id, [roomName, unsubscribe]);
|
|
44
|
+
|
|
45
|
+
return Promise.resolve(id);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public unsubscribe(subscriptionId: number): void {
|
|
49
|
+
const [roomName, unsubscribe] =
|
|
50
|
+
this._unsubscriptions.get(subscriptionId) ?? [];
|
|
51
|
+
|
|
52
|
+
if (!roomName || !unsubscribe) return;
|
|
53
|
+
|
|
54
|
+
const refs = this._subscriptionsRefs.get(roomName);
|
|
55
|
+
|
|
56
|
+
if (!refs) return;
|
|
57
|
+
|
|
58
|
+
if (refs.length === 1) {
|
|
59
|
+
this._subjects.delete(roomName);
|
|
60
|
+
this._subscriptionsRefs.delete(roomName);
|
|
61
|
+
this._unsubscriptions.delete(subscriptionId);
|
|
62
|
+
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const index = refs.indexOf(subscriptionId);
|
|
67
|
+
|
|
68
|
+
this._subscriptionsRefs.set(
|
|
69
|
+
roomName,
|
|
70
|
+
index === -1
|
|
71
|
+
? refs
|
|
72
|
+
: [...refs.slice(0, index), ...refs.slice(index + 1)]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
unsubscribe();
|
|
76
|
+
|
|
77
|
+
this._unsubscriptions.delete(subscriptionId);
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/authorize.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import hkdf from "@panva/hkdf";
|
|
2
|
+
import { BaseUser } from "@pluv/types";
|
|
3
|
+
import { EncryptJWT, jwtDecrypt } from "jose";
|
|
4
|
+
import { AbstractPlatform } from "./AbstractPlatform";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MAX_AGE_MS = 60_000;
|
|
7
|
+
|
|
8
|
+
export const getEncryptionKey = async (secret: string): Promise<Uint8Array> => {
|
|
9
|
+
return await hkdf(
|
|
10
|
+
"sha256",
|
|
11
|
+
secret,
|
|
12
|
+
"",
|
|
13
|
+
"Pluv.io Generated Encryption Key",
|
|
14
|
+
32
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface JWT<TUser extends BaseUser> {
|
|
19
|
+
room: string;
|
|
20
|
+
sub: string;
|
|
21
|
+
user: TUser;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface JWTEncodeParams<TUser extends BaseUser> {
|
|
25
|
+
maxAge?: number;
|
|
26
|
+
room: string;
|
|
27
|
+
user: TUser;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AuthorizeParams {
|
|
31
|
+
platform: AbstractPlatform;
|
|
32
|
+
secret: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AuthorizeModule {
|
|
36
|
+
decode: <TUser extends BaseUser>(jwt: string) => Promise<TUser | null>;
|
|
37
|
+
encode: <TUser extends BaseUser>(
|
|
38
|
+
params: JWTEncodeParams<TUser>
|
|
39
|
+
) => Promise<string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const now = () => (Date.now() / 1_000) | 0;
|
|
43
|
+
|
|
44
|
+
export const authorize = (params: AuthorizeParams) => {
|
|
45
|
+
const { platform, secret } = params;
|
|
46
|
+
|
|
47
|
+
const encode = async <TUser extends BaseUser>(
|
|
48
|
+
encodeParams: JWTEncodeParams<TUser>
|
|
49
|
+
): Promise<string> => {
|
|
50
|
+
const { maxAge = DEFAULT_MAX_AGE_MS, room, user } = encodeParams;
|
|
51
|
+
|
|
52
|
+
const encryptionSecret = await getEncryptionKey(secret);
|
|
53
|
+
|
|
54
|
+
const token = await new EncryptJWT({
|
|
55
|
+
room,
|
|
56
|
+
sub: room ? `${room}|${user.id}` : user.id,
|
|
57
|
+
user,
|
|
58
|
+
})
|
|
59
|
+
.setProtectedHeader({ alg: "dir", enc: "A256GCM" })
|
|
60
|
+
.setIssuedAt()
|
|
61
|
+
.setExpirationTime(now() + maxAge)
|
|
62
|
+
.setJti(platform.randomUUID())
|
|
63
|
+
.encrypt(encryptionSecret);
|
|
64
|
+
|
|
65
|
+
return token.toString();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const decode = async <TUser extends BaseUser>(
|
|
69
|
+
jwt: string
|
|
70
|
+
): Promise<JWT<TUser> | null> => {
|
|
71
|
+
const encryptionSecret = await getEncryptionKey(secret);
|
|
72
|
+
|
|
73
|
+
const { payload } = await jwtDecrypt(jwt, encryptionSecret, {
|
|
74
|
+
clockTolerance: 15,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return (payload as any) ?? null;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return { decode, encode };
|
|
81
|
+
};
|
package/src/createIO.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BaseClientEventRecord,
|
|
3
|
+
BaseIOEventRecord,
|
|
4
|
+
BaseUser,
|
|
5
|
+
IOAuthorize,
|
|
6
|
+
JsonObject,
|
|
7
|
+
} from "@pluv/types";
|
|
8
|
+
import type { AbstractPlatform } from "./AbstractPlatform";
|
|
9
|
+
import { PluvIO } from "./PluvIO";
|
|
10
|
+
|
|
11
|
+
export interface CreateIOParams<
|
|
12
|
+
TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
|
|
13
|
+
TContext extends JsonObject = {},
|
|
14
|
+
TAuthorizeUser extends BaseUser = BaseUser,
|
|
15
|
+
TAuthorizeRequired extends boolean = false
|
|
16
|
+
> {
|
|
17
|
+
authorize?: IOAuthorize<TAuthorizeUser, TAuthorizeRequired>;
|
|
18
|
+
context?: TContext;
|
|
19
|
+
debug?: boolean;
|
|
20
|
+
platform: TPlatform;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const createIO = <
|
|
24
|
+
TPlatform extends AbstractPlatform<any> = AbstractPlatform<any>,
|
|
25
|
+
TContext extends JsonObject = {},
|
|
26
|
+
TAuthorizeUser extends BaseUser = BaseUser,
|
|
27
|
+
TAuthorizeRequired extends boolean = false
|
|
28
|
+
>(
|
|
29
|
+
params: CreateIOParams<
|
|
30
|
+
TPlatform,
|
|
31
|
+
TContext,
|
|
32
|
+
TAuthorizeUser,
|
|
33
|
+
TAuthorizeRequired
|
|
34
|
+
>
|
|
35
|
+
): PluvIO<
|
|
36
|
+
TPlatform,
|
|
37
|
+
IOAuthorize<TAuthorizeUser, TAuthorizeRequired>,
|
|
38
|
+
TContext,
|
|
39
|
+
BaseClientEventRecord,
|
|
40
|
+
BaseIOEventRecord<IOAuthorize<TAuthorizeUser, TAuthorizeRequired>>
|
|
41
|
+
> => {
|
|
42
|
+
const { authorize, context, debug, platform } = params;
|
|
43
|
+
|
|
44
|
+
return new PluvIO<
|
|
45
|
+
TPlatform,
|
|
46
|
+
IOAuthorize<TAuthorizeUser, TAuthorizeRequired>,
|
|
47
|
+
TContext,
|
|
48
|
+
BaseClientEventRecord,
|
|
49
|
+
BaseIOEventRecord<IOAuthorize<TAuthorizeUser, TAuthorizeRequired>>
|
|
50
|
+
>({ authorize, context, debug, platform });
|
|
51
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
BaseUser,
|
|
3
|
+
EventMessage,
|
|
4
|
+
EventRecord,
|
|
5
|
+
IOEventMessage,
|
|
6
|
+
} from "@pluv/types";
|
|
7
|
+
export type {
|
|
8
|
+
ConvertWebSocketConfig,
|
|
9
|
+
InferWebSocketType,
|
|
10
|
+
} from "./AbstractPlatform";
|
|
11
|
+
export { AbstractPersistance } from "./AbstractPersistance";
|
|
12
|
+
export { AbstractPlatform } from "./AbstractPlatform";
|
|
13
|
+
export type { IOPubSubEventMessage } from "./AbstractPubSub";
|
|
14
|
+
export { AbstractPubSub } from "./AbstractPubSub";
|
|
15
|
+
export type {
|
|
16
|
+
AbstractCloseEvent,
|
|
17
|
+
AbstractErrorEvent,
|
|
18
|
+
AbstractEvent,
|
|
19
|
+
AbstractMessageEvent,
|
|
20
|
+
AbstractEventMap,
|
|
21
|
+
AbstractListener,
|
|
22
|
+
AbstractWebSocketConfig,
|
|
23
|
+
AbstractWebSocketHandleErrorParams,
|
|
24
|
+
} from "./AbstractWebSocket";
|
|
25
|
+
export { AbstractWebSocket } from "./AbstractWebSocket";
|
|
26
|
+
export type {
|
|
27
|
+
AuthorizeModule,
|
|
28
|
+
AuthorizeParams,
|
|
29
|
+
JWT,
|
|
30
|
+
JWTEncodeParams,
|
|
31
|
+
} from "./authorize";
|
|
32
|
+
export { createIO } from "./createIO";
|
|
33
|
+
export type { IORoom } from "./IORoom";
|
|
34
|
+
export type { InferIORoom, PluvIO } from "./PluvIO";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { YjsDoc } from "@pluv/crdt-yjs";
|
|
2
|
+
import type {
|
|
3
|
+
EventRecord,
|
|
4
|
+
Id,
|
|
5
|
+
InputZodLike,
|
|
6
|
+
JsonObject,
|
|
7
|
+
MaybePromise,
|
|
8
|
+
} from "@pluv/types";
|
|
9
|
+
import type { AbstractWebSocket } from "./AbstractWebSocket";
|
|
10
|
+
|
|
11
|
+
export interface Console {
|
|
12
|
+
debug(...data: any[]): void;
|
|
13
|
+
error(...data: any[]): void;
|
|
14
|
+
info(...data: any[]): void;
|
|
15
|
+
log(...data: any[]): void;
|
|
16
|
+
warn(...data: any[]): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare global {
|
|
20
|
+
var console: {
|
|
21
|
+
log: (...data: any[]) => void;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface EventConfig<
|
|
26
|
+
TContext extends JsonObject = {},
|
|
27
|
+
TData extends JsonObject = {},
|
|
28
|
+
TResult extends EventRecord<string, any> = {}
|
|
29
|
+
> {
|
|
30
|
+
input?: InputZodLike<TData>;
|
|
31
|
+
options?: SendMessageOptions;
|
|
32
|
+
resolver: EventResolver<TContext, TData, TResult>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type EventConfigType = "broadcast" | "self" | "sync";
|
|
36
|
+
|
|
37
|
+
export type EventResolver<
|
|
38
|
+
TContext extends JsonObject = {},
|
|
39
|
+
TData extends JsonObject = {},
|
|
40
|
+
TResult extends EventRecord<string, any> = {}
|
|
41
|
+
> = (
|
|
42
|
+
data: TData,
|
|
43
|
+
context: EventResolverContext<TContext>
|
|
44
|
+
) => MaybePromise<TResult | void>;
|
|
45
|
+
|
|
46
|
+
export interface EventResolverContext<TContext extends JsonObject = {}> {
|
|
47
|
+
context: TContext;
|
|
48
|
+
doc: YjsDoc<any>;
|
|
49
|
+
room: string;
|
|
50
|
+
/**
|
|
51
|
+
* !HACK
|
|
52
|
+
* @description Session should only not exist when invoking the event as a
|
|
53
|
+
* "sync" event on another machine where the session doesn't exist. There must
|
|
54
|
+
* be a better way to handle this, but going to make this nullable for ease
|
|
55
|
+
* for now.
|
|
56
|
+
*
|
|
57
|
+
* `session` should not be referenced most of the time anyhow (outside of
|
|
58
|
+
* internal to this library).
|
|
59
|
+
* @author leedavidcs
|
|
60
|
+
* @date December 25, 2022
|
|
61
|
+
*/
|
|
62
|
+
session: WebSocketSession | null;
|
|
63
|
+
sessions: Map<string, WebSocketSession>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type InferEventConfig<
|
|
67
|
+
TContext extends JsonObject = {},
|
|
68
|
+
TInput extends EventRecord<string, any> = {},
|
|
69
|
+
TOutput extends EventRecord<string, any> = {}
|
|
70
|
+
> = {
|
|
71
|
+
[P in keyof TInput]: P extends string
|
|
72
|
+
? Id<EventConfig<TContext, TInput[P], TOutput>>
|
|
73
|
+
: never;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type SendMessageOptions =
|
|
77
|
+
| { type?: "broadcast"; sessionIds?: readonly string[] }
|
|
78
|
+
| { type: "self" }
|
|
79
|
+
| { type: "sync" };
|
|
80
|
+
|
|
81
|
+
export interface WebSocketSessionTimers {
|
|
82
|
+
ping: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface WebSocketSession {
|
|
86
|
+
id: string;
|
|
87
|
+
presence: JsonObject | null;
|
|
88
|
+
quit: boolean;
|
|
89
|
+
room: string;
|
|
90
|
+
timers: WebSocketSessionTimers;
|
|
91
|
+
webSocket: AbstractWebSocket;
|
|
92
|
+
user: JsonObject | null;
|
|
93
|
+
}
|