@triformine/nexis-sdk 0.1.0-next.6d96464
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/README.md +48 -0
- package/dist/cjs/client.js +746 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/codec.js +57 -0
- package/dist/cjs/codec.js.map +1 -0
- package/dist/cjs/index.js +24 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/patch.js +110 -0
- package/dist/cjs/patch.js.map +1 -0
- package/dist/cjs/rpc.js +69 -0
- package/dist/cjs/rpc.js.map +1 -0
- package/dist/cjs/types.js +6 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/esm/client.js +719 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/codec.js +36 -0
- package/dist/esm/codec.js.map +1 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/patch.js +86 -0
- package/dist/esm/patch.js.map +1 -0
- package/dist/esm/rpc.js +51 -0
- package/dist/esm/rpc.js.map +1 -0
- package/dist/esm/types.js +3 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/types/client.d.ts +78 -0
- package/dist/types/codec.d.ts +19 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/patch.d.ts +5 -0
- package/dist/types/rpc.d.ts +14 -0
- package/dist/types/types.d.ts +75 -0
- package/package.json +53 -0
- package/src/client.ts +949 -0
- package/src/codec.ts +48 -0
- package/src/index.ts +5 -0
- package/src/patch.ts +128 -0
- package/src/rpc.ts +54 -0
- package/src/types.ts +82 -0
package/src/codec.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Packr, Unpackr } from "msgpackr";
|
|
2
|
+
import type { Envelope } from "./types";
|
|
3
|
+
|
|
4
|
+
export interface Codec {
|
|
5
|
+
readonly name: "json" | "msgpack";
|
|
6
|
+
encode(message: Envelope): Uint8Array;
|
|
7
|
+
decode(bytes: Uint8Array): Envelope;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class JsonCodec implements Codec {
|
|
11
|
+
readonly name = "json" as const;
|
|
12
|
+
|
|
13
|
+
encode(message: Envelope): Uint8Array {
|
|
14
|
+
return new TextEncoder().encode(JSON.stringify(message));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
decode(bytes: Uint8Array): Envelope {
|
|
18
|
+
const text = new TextDecoder().decode(bytes);
|
|
19
|
+
const parsed = JSON.parse(text);
|
|
20
|
+
return parsed as Envelope;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class MsgpackCodec implements Codec {
|
|
25
|
+
readonly name = "msgpack" as const;
|
|
26
|
+
private readonly packr = new Packr({
|
|
27
|
+
useRecords: false,
|
|
28
|
+
structuredClone: false,
|
|
29
|
+
bundleStrings: false,
|
|
30
|
+
maxSharedStructures: 0,
|
|
31
|
+
});
|
|
32
|
+
private readonly unpackr = new Unpackr({
|
|
33
|
+
useRecords: false,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
encode(message: Envelope): Uint8Array {
|
|
37
|
+
return this.packr.pack(message);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
decode(bytes: Uint8Array): Envelope {
|
|
41
|
+
const decoded = this.unpackr.unpack(bytes);
|
|
42
|
+
return decoded as Envelope;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function codecFor(name: "json" | "msgpack"): Codec {
|
|
47
|
+
return name === "msgpack" ? new MsgpackCodec() : new JsonCodec();
|
|
48
|
+
}
|
package/src/index.ts
ADDED
package/src/patch.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { PatchOp, StatePatchPayload, StateSnapshotPayload } from "./types";
|
|
2
|
+
|
|
3
|
+
function keyFromPath(path: string): string {
|
|
4
|
+
if (!path.startsWith("/")) {
|
|
5
|
+
throw new Error(`Invalid patch path: ${path}`);
|
|
6
|
+
}
|
|
7
|
+
const key = path.slice(1);
|
|
8
|
+
if (!key || key.includes("/")) {
|
|
9
|
+
throw new Error(`Invalid patch path: ${path}`);
|
|
10
|
+
}
|
|
11
|
+
return key.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function applyPatch<T extends Record<string, unknown>>(
|
|
15
|
+
state: T,
|
|
16
|
+
patch: PatchOp[],
|
|
17
|
+
): T {
|
|
18
|
+
const next: Record<string, unknown> = { ...state };
|
|
19
|
+
|
|
20
|
+
for (const op of patch) {
|
|
21
|
+
const key = keyFromPath(op.path);
|
|
22
|
+
if (op.op === "set") {
|
|
23
|
+
next[key] = op.value;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
delete next[key];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return next as T;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function canonicalize(value: unknown): unknown {
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
return value.map((item) => canonicalize(item));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (value && typeof value === "object") {
|
|
38
|
+
const sortedEntries = Object.entries(value as Record<string, unknown>).sort(
|
|
39
|
+
([left], [right]) => left.localeCompare(right),
|
|
40
|
+
);
|
|
41
|
+
const normalized: Record<string, unknown> = {};
|
|
42
|
+
for (const [key, item] of sortedEntries) {
|
|
43
|
+
normalized[key] = canonicalize(item);
|
|
44
|
+
}
|
|
45
|
+
return normalized;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function computeStateChecksum(state: unknown): Promise<string> {
|
|
52
|
+
const cryptoApi = globalThis.crypto?.subtle;
|
|
53
|
+
if (!cryptoApi) {
|
|
54
|
+
throw new Error("crypto.subtle is unavailable");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const canonicalJson = JSON.stringify(canonicalize(state));
|
|
58
|
+
const bytes = new TextEncoder().encode(canonicalJson);
|
|
59
|
+
const digest = await cryptoApi.digest("SHA-256", bytes);
|
|
60
|
+
const hashBytes = new Uint8Array(digest);
|
|
61
|
+
|
|
62
|
+
return Array.from(hashBytes)
|
|
63
|
+
.map((value) => value.toString(16).padStart(2, "0"))
|
|
64
|
+
.join("");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parsePatchPayload(payload: unknown): StatePatchPayload | null {
|
|
68
|
+
if (Array.isArray(payload)) {
|
|
69
|
+
return {
|
|
70
|
+
seq: 0,
|
|
71
|
+
checksum: undefined,
|
|
72
|
+
ops: payload as PatchOp[],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!payload || typeof payload !== "object") {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const candidate = payload as {
|
|
81
|
+
seq?: unknown;
|
|
82
|
+
checksum?: unknown;
|
|
83
|
+
ops?: unknown;
|
|
84
|
+
};
|
|
85
|
+
if (
|
|
86
|
+
typeof candidate.seq !== "number" ||
|
|
87
|
+
!Array.isArray(candidate.ops) ||
|
|
88
|
+
(candidate.checksum !== undefined && typeof candidate.checksum !== "string")
|
|
89
|
+
) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
seq: candidate.seq,
|
|
95
|
+
checksum: candidate.checksum as string | undefined,
|
|
96
|
+
ops: candidate.ops as PatchOp[],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function parseSnapshotPayload(
|
|
101
|
+
payload: unknown,
|
|
102
|
+
): StateSnapshotPayload | null {
|
|
103
|
+
if (!payload || typeof payload !== "object") {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const candidate = payload as {
|
|
108
|
+
seq?: unknown;
|
|
109
|
+
checksum?: unknown;
|
|
110
|
+
state?: unknown;
|
|
111
|
+
};
|
|
112
|
+
if (
|
|
113
|
+
typeof candidate.seq !== "number" ||
|
|
114
|
+
(candidate.checksum !== undefined &&
|
|
115
|
+
typeof candidate.checksum !== "string") ||
|
|
116
|
+
!candidate.state ||
|
|
117
|
+
typeof candidate.state !== "object" ||
|
|
118
|
+
Array.isArray(candidate.state)
|
|
119
|
+
) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
seq: candidate.seq,
|
|
125
|
+
checksum: candidate.checksum as string | undefined,
|
|
126
|
+
state: candidate.state as Record<string, unknown>,
|
|
127
|
+
};
|
|
128
|
+
}
|
package/src/rpc.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Envelope } from "./types";
|
|
2
|
+
|
|
3
|
+
export class UnknownRidError extends Error {
|
|
4
|
+
constructor(rid: string) {
|
|
5
|
+
super(`Unknown RPC rid: ${rid}`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class RpcClient {
|
|
10
|
+
private nextId = 1;
|
|
11
|
+
private pending = new Map<
|
|
12
|
+
string,
|
|
13
|
+
{ resolve: (payload: unknown) => void; reject: (error: Error) => void }
|
|
14
|
+
>();
|
|
15
|
+
|
|
16
|
+
createRequest(
|
|
17
|
+
type: string,
|
|
18
|
+
payload: unknown,
|
|
19
|
+
room?: string,
|
|
20
|
+
): { message: Envelope; promise: Promise<unknown> } {
|
|
21
|
+
const rid = `rpc-${this.nextId++}`;
|
|
22
|
+
const message: Envelope = { v: 1, t: type, rid, p: payload };
|
|
23
|
+
if (room !== undefined) {
|
|
24
|
+
message.room = room;
|
|
25
|
+
}
|
|
26
|
+
const promise = new Promise<unknown>((resolve, reject) => {
|
|
27
|
+
this.pending.set(rid, { resolve, reject });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return { message, promise };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
resolveResponse(response: Envelope): void {
|
|
34
|
+
const rid = response.rid;
|
|
35
|
+
if (!rid) {
|
|
36
|
+
throw new UnknownRidError("missing");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const pending = this.pending.get(rid);
|
|
40
|
+
if (!pending) {
|
|
41
|
+
throw new UnknownRidError(rid);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.pending.delete(rid);
|
|
45
|
+
pending.resolve(response.p);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
rejectAll(error: Error): void {
|
|
49
|
+
for (const pending of this.pending.values()) {
|
|
50
|
+
pending.reject(error);
|
|
51
|
+
}
|
|
52
|
+
this.pending.clear();
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type Envelope = {
|
|
2
|
+
v: number;
|
|
3
|
+
t: string;
|
|
4
|
+
rid?: string;
|
|
5
|
+
room?: string;
|
|
6
|
+
p?: unknown;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type HandshakeRequest = {
|
|
10
|
+
v: number;
|
|
11
|
+
codecs: string[];
|
|
12
|
+
project_id: string;
|
|
13
|
+
token: string;
|
|
14
|
+
session_id?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ConnectOptions = {
|
|
18
|
+
projectId?: string;
|
|
19
|
+
token?: string;
|
|
20
|
+
codecs?: Array<"msgpack" | "json">;
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
autoJoinMatchedRoom?: boolean;
|
|
23
|
+
autoReconnect?: boolean;
|
|
24
|
+
reconnectInitialDelayMs?: number;
|
|
25
|
+
reconnectMaxDelayMs?: number;
|
|
26
|
+
reconnectMaxAttempts?: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type RoomSummary = {
|
|
30
|
+
id: string;
|
|
31
|
+
room_type: string;
|
|
32
|
+
members: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type RoomListResponse = {
|
|
36
|
+
ok: boolean;
|
|
37
|
+
rooms: RoomSummary[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type MatchFound = {
|
|
41
|
+
room: string;
|
|
42
|
+
roomType: string;
|
|
43
|
+
size: number;
|
|
44
|
+
participants: string[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type MatchmakingQueueResponse = {
|
|
48
|
+
ok: boolean;
|
|
49
|
+
queued?: boolean;
|
|
50
|
+
matched?: boolean;
|
|
51
|
+
room_type?: string;
|
|
52
|
+
size?: number;
|
|
53
|
+
position?: number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type MatchmakingDequeueResponse = {
|
|
57
|
+
ok: boolean;
|
|
58
|
+
removed: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type PatchOp =
|
|
62
|
+
| { op: "set"; path: string; value: unknown }
|
|
63
|
+
| { op: "del"; path: string };
|
|
64
|
+
|
|
65
|
+
export type StatePatchPayload = {
|
|
66
|
+
seq: number;
|
|
67
|
+
checksum?: string;
|
|
68
|
+
ops: PatchOp[];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type StateSnapshotPayload = {
|
|
72
|
+
seq: number;
|
|
73
|
+
checksum?: string;
|
|
74
|
+
state: Record<string, unknown>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type RoomMessageType = string | number;
|
|
78
|
+
|
|
79
|
+
export type RoomMessagePayload = {
|
|
80
|
+
type: RoomMessageType;
|
|
81
|
+
data: unknown;
|
|
82
|
+
};
|