@syncular/core 0.0.6-95 → 0.1.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/dist/blobs.d.ts +14 -7
- package/dist/blobs.d.ts.map +1 -1
- package/dist/blobs.js.map +1 -1
- package/dist/column-codecs.d.ts +3 -0
- package/dist/column-codecs.d.ts.map +1 -1
- package/dist/column-codecs.js +32 -4
- package/dist/column-codecs.js.map +1 -1
- package/dist/conflict.js.map +1 -1
- package/dist/database.js +1 -1
- package/dist/database.js.map +1 -1
- package/dist/error-responses.d.ts +393 -0
- package/dist/error-responses.d.ts.map +1 -0
- package/dist/error-responses.js +393 -0
- package/dist/error-responses.js.map +1 -0
- package/dist/http/api-client.d.ts +11 -0
- package/dist/http/api-client.d.ts.map +1 -0
- package/dist/http/api-client.js +30 -0
- package/dist/http/api-client.js.map +1 -0
- package/dist/http/blob.d.ts +6 -0
- package/dist/http/blob.d.ts.map +1 -0
- package/dist/http/blob.js +46 -0
- package/dist/http/blob.js.map +1 -0
- package/dist/http/generated/api.d.ts +5241 -0
- package/dist/http/generated/api.d.ts.map +1 -0
- package/dist/http/generated/api.js +6 -0
- package/dist/http/generated/api.js.map +1 -0
- package/dist/http/index.d.ts +18 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +212 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/shared.d.ts +45 -0
- package/dist/http/shared.d.ts.map +1 -0
- package/dist/http/shared.js +122 -0
- package/dist/http/shared.js.map +1 -0
- package/dist/http/transport-client.d.ts +31 -0
- package/dist/http/transport-client.d.ts.map +1 -0
- package/dist/http/transport-client.js +193 -0
- package/dist/http/transport-client.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -15
- package/dist/index.js.map +1 -1
- package/dist/kysely-column-codecs.d.ts +17 -1
- package/dist/kysely-column-codecs.d.ts.map +1 -1
- package/dist/kysely-column-codecs.js +56 -70
- package/dist/kysely-column-codecs.js.map +1 -1
- package/dist/logger.js +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/schemas/blobs.d.ts +7 -1
- package/dist/schemas/blobs.d.ts.map +1 -1
- package/dist/schemas/blobs.js +1 -0
- package/dist/schemas/blobs.js.map +1 -1
- package/dist/schemas/common.d.ts +73 -1
- package/dist/schemas/common.d.ts.map +1 -1
- package/dist/schemas/common.js +38 -0
- package/dist/schemas/common.js.map +1 -1
- package/dist/schemas/index.js +3 -3
- package/dist/schemas/sync.d.ts +712 -38
- package/dist/schemas/sync.d.ts.map +1 -1
- package/dist/schemas/sync.js +209 -5
- package/dist/schemas/sync.js.map +1 -1
- package/dist/scopes/index.d.ts +5 -0
- package/dist/scopes/index.d.ts.map +1 -1
- package/dist/scopes/index.js +27 -0
- package/dist/scopes/index.js.map +1 -1
- package/dist/sentry.d.ts +31 -0
- package/dist/sentry.d.ts.map +1 -0
- package/dist/sentry.js +147 -0
- package/dist/sentry.js.map +1 -0
- package/dist/snapshot-chunks.d.ts +104 -20
- package/dist/snapshot-chunks.d.ts.map +1 -1
- package/dist/snapshot-chunks.js +665 -72
- package/dist/snapshot-chunks.js.map +1 -1
- package/dist/sync-packs.d.ts +33 -0
- package/dist/sync-packs.d.ts.map +1 -0
- package/dist/sync-packs.js +821 -0
- package/dist/sync-packs.js.map +1 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +3 -2
- package/dist/telemetry.js.map +1 -1
- package/dist/transforms.js.map +1 -1
- package/dist/types.d.ts +48 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/bytes.d.ts +4 -0
- package/dist/utils/bytes.d.ts.map +1 -0
- package/dist/utils/bytes.js +66 -0
- package/dist/utils/bytes.js.map +1 -0
- package/dist/utils/compression.d.ts +11 -3
- package/dist/utils/compression.d.ts.map +1 -1
- package/dist/utils/compression.js +102 -50
- package/dist/utils/compression.js.map +1 -1
- package/dist/utils/crypto.d.ts +7 -1
- package/dist/utils/crypto.d.ts.map +1 -1
- package/dist/utils/crypto.js +89 -7
- package/dist/utils/crypto.js.map +1 -1
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +4 -1
- package/dist/utils/id.js.map +1 -1
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +9 -4
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/internal-runtime.d.ts +16 -0
- package/dist/utils/internal-runtime.d.ts.map +1 -0
- package/dist/utils/internal-runtime.js +14 -0
- package/dist/utils/internal-runtime.js.map +1 -0
- package/dist/utils/object.js.map +1 -1
- package/dist/utils/realtime-change-index.d.ts +9 -0
- package/dist/utils/realtime-change-index.d.ts.map +1 -0
- package/dist/utils/realtime-change-index.js +36 -0
- package/dist/utils/realtime-change-index.js.map +1 -0
- package/dist/utils/realtime-connection-registry.d.ts +40 -0
- package/dist/utils/realtime-connection-registry.d.ts.map +1 -0
- package/dist/utils/realtime-connection-registry.js +237 -0
- package/dist/utils/realtime-connection-registry.js.map +1 -0
- package/dist/utils/table-registry.d.ts +10 -0
- package/dist/utils/table-registry.d.ts.map +1 -0
- package/dist/utils/table-registry.js +54 -0
- package/dist/utils/table-registry.js.map +1 -0
- package/dist/utils/url.d.ts +3 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/dist/utils/url.js +20 -0
- package/dist/utils/url.js.map +1 -0
- package/package.json +52 -7
- package/src/blobs.ts +24 -7
- package/src/column-codecs.ts +36 -8
- package/src/error-responses.ts +423 -0
- package/src/http/api-client.ts +39 -0
- package/src/http/blob.ts +95 -0
- package/src/http/generated/api.ts +5241 -0
- package/src/http/index.ts +365 -0
- package/src/http/shared.ts +220 -0
- package/src/http/transport-client.ts +314 -0
- package/src/index.ts +4 -0
- package/src/kysely-column-codecs.ts +81 -86
- package/src/schemas/blobs.ts +10 -1
- package/src/schemas/common.ts +44 -0
- package/src/schemas/sync.ts +298 -7
- package/src/scopes/index.ts +42 -0
- package/src/sentry.ts +200 -0
- package/src/snapshot-chunks.ts +921 -69
- package/src/sync-packs.ts +1096 -0
- package/src/telemetry.ts +7 -2
- package/src/types.ts +63 -8
- package/src/utils/bytes.ts +75 -0
- package/src/utils/compression.ts +147 -51
- package/src/utils/crypto.ts +120 -7
- package/src/utils/id.ts +11 -1
- package/src/utils/index.ts +5 -0
- package/src/utils/internal-runtime.ts +39 -0
- package/src/utils/realtime-change-index.ts +44 -0
- package/src/utils/realtime-connection-registry.ts +286 -0
- package/src/utils/table-registry.ts +67 -0
- package/src/utils/url.ts +29 -0
- package/src/__tests__/compression.test.ts +0 -54
- package/src/__tests__/conflict.test.ts +0 -325
- package/src/__tests__/telemetry.test.ts +0 -170
- package/src/__tests__/utils.test.ts +0 -27
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface RealtimeChangeScopeEntry<T> {
|
|
2
|
+
item: T;
|
|
3
|
+
scopeKeys: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface RealtimeChangeScopeIndex<T> {
|
|
7
|
+
selectForScopeKeys(scopeKeys: Iterable<string>): T[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createRealtimeChangeScopeIndex<T>(
|
|
11
|
+
entries: readonly RealtimeChangeScopeEntry<T>[]
|
|
12
|
+
): RealtimeChangeScopeIndex<T> {
|
|
13
|
+
const items = entries.map((entry) => entry.item);
|
|
14
|
+
const indexesByScopeKey = new Map<string, number[]>();
|
|
15
|
+
|
|
16
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
17
|
+
const seenScopeKeys = new Set<string>();
|
|
18
|
+
for (const scopeKey of entries[index]!.scopeKeys) {
|
|
19
|
+
if (!scopeKey || seenScopeKeys.has(scopeKey)) continue;
|
|
20
|
+
seenScopeKeys.add(scopeKey);
|
|
21
|
+
let indexes = indexesByScopeKey.get(scopeKey);
|
|
22
|
+
if (!indexes) {
|
|
23
|
+
indexes = [];
|
|
24
|
+
indexesByScopeKey.set(scopeKey, indexes);
|
|
25
|
+
}
|
|
26
|
+
indexes.push(index);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
selectForScopeKeys(scopeKeys: Iterable<string>): T[] {
|
|
32
|
+
const selectedIndexes = new Set<number>();
|
|
33
|
+
for (const scopeKey of scopeKeys) {
|
|
34
|
+
const indexes = indexesByScopeKey.get(scopeKey);
|
|
35
|
+
if (!indexes) continue;
|
|
36
|
+
for (const index of indexes) selectedIndexes.add(index);
|
|
37
|
+
}
|
|
38
|
+
if (selectedIndexes.size === 0) return [];
|
|
39
|
+
return Array.from(selectedIndexes)
|
|
40
|
+
.sort((a, b) => a - b)
|
|
41
|
+
.map((index) => items[index]!);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
export interface RealtimeConnection {
|
|
2
|
+
readonly clientId: string;
|
|
3
|
+
readonly ownerKey: string;
|
|
4
|
+
readonly isOpen: boolean;
|
|
5
|
+
sendHeartbeat(): void;
|
|
6
|
+
close(code?: number, reason?: string): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class RealtimeConnectionRegistry<
|
|
10
|
+
TConnection extends RealtimeConnection,
|
|
11
|
+
> {
|
|
12
|
+
private connectionsByOwnerKey = new Map<string, Set<TConnection>>();
|
|
13
|
+
private connectionsByClientId = new Map<string, Set<TConnection>>();
|
|
14
|
+
private scopeKeysByOwnerKey = new Map<string, Set<string>>();
|
|
15
|
+
private connectionsByScopeKey = new Map<string, Set<TConnection>>();
|
|
16
|
+
|
|
17
|
+
private readonly heartbeatIntervalMs: number;
|
|
18
|
+
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
19
|
+
private readonly onOwnerDisconnected?: (ownerKey: string) => void;
|
|
20
|
+
|
|
21
|
+
constructor(options?: {
|
|
22
|
+
heartbeatIntervalMs?: number;
|
|
23
|
+
onOwnerDisconnected?: (ownerKey: string) => void;
|
|
24
|
+
}) {
|
|
25
|
+
this.heartbeatIntervalMs = options?.heartbeatIntervalMs ?? 30_000;
|
|
26
|
+
this.onOwnerDisconnected = options?.onOwnerDisconnected;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
register(
|
|
30
|
+
connection: TConnection,
|
|
31
|
+
initialScopeKeys: string[] = []
|
|
32
|
+
): () => void {
|
|
33
|
+
const ownerKey = connection.ownerKey;
|
|
34
|
+
let ownerConns = this.connectionsByOwnerKey.get(ownerKey);
|
|
35
|
+
if (!ownerConns) {
|
|
36
|
+
ownerConns = new Set();
|
|
37
|
+
this.connectionsByOwnerKey.set(ownerKey, ownerConns);
|
|
38
|
+
}
|
|
39
|
+
ownerConns.add(connection);
|
|
40
|
+
|
|
41
|
+
let clientConns = this.connectionsByClientId.get(connection.clientId);
|
|
42
|
+
if (!clientConns) {
|
|
43
|
+
clientConns = new Set();
|
|
44
|
+
this.connectionsByClientId.set(connection.clientId, clientConns);
|
|
45
|
+
}
|
|
46
|
+
clientConns.add(connection);
|
|
47
|
+
|
|
48
|
+
if (!this.scopeKeysByOwnerKey.has(ownerKey)) {
|
|
49
|
+
this.scopeKeysByOwnerKey.set(ownerKey, new Set(initialScopeKeys));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const scopeKeys = this.scopeKeysByOwnerKey.get(ownerKey);
|
|
53
|
+
if (scopeKeys) {
|
|
54
|
+
for (const key of scopeKeys) {
|
|
55
|
+
let scopedConns = this.connectionsByScopeKey.get(key);
|
|
56
|
+
if (!scopedConns) {
|
|
57
|
+
scopedConns = new Set();
|
|
58
|
+
this.connectionsByScopeKey.set(key, scopedConns);
|
|
59
|
+
}
|
|
60
|
+
scopedConns.add(connection);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.ensureHeartbeat();
|
|
65
|
+
return () => {
|
|
66
|
+
this.unregister(connection);
|
|
67
|
+
this.ensureHeartbeat();
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
updateOwnerScopeKeys(ownerKey: string, scopeKeys: string[]): void {
|
|
72
|
+
const conns = this.connectionsByOwnerKey.get(ownerKey);
|
|
73
|
+
if (!conns || conns.size === 0) return;
|
|
74
|
+
|
|
75
|
+
const next = new Set(scopeKeys);
|
|
76
|
+
const prev = this.scopeKeysByOwnerKey.get(ownerKey) ?? new Set<string>();
|
|
77
|
+
|
|
78
|
+
if (prev.size === next.size) {
|
|
79
|
+
let unchanged = true;
|
|
80
|
+
for (const key of prev) {
|
|
81
|
+
if (!next.has(key)) {
|
|
82
|
+
unchanged = false;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (unchanged) return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.scopeKeysByOwnerKey.set(ownerKey, next);
|
|
90
|
+
|
|
91
|
+
for (const key of prev) {
|
|
92
|
+
if (next.has(key)) continue;
|
|
93
|
+
const scopedConns = this.connectionsByScopeKey.get(key);
|
|
94
|
+
if (!scopedConns) continue;
|
|
95
|
+
for (const conn of conns) scopedConns.delete(conn);
|
|
96
|
+
if (scopedConns.size === 0) this.connectionsByScopeKey.delete(key);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const key of next) {
|
|
100
|
+
if (prev.has(key)) continue;
|
|
101
|
+
let scopedConns = this.connectionsByScopeKey.get(key);
|
|
102
|
+
if (!scopedConns) {
|
|
103
|
+
scopedConns = new Set();
|
|
104
|
+
this.connectionsByScopeKey.set(key, scopedConns);
|
|
105
|
+
}
|
|
106
|
+
for (const conn of conns) scopedConns.add(conn);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
isOwnerSubscribedToScopeKey(ownerKey: string, scopeKey: string): boolean {
|
|
111
|
+
const scopeKeys = this.scopeKeysByOwnerKey.get(ownerKey);
|
|
112
|
+
if (!scopeKeys || scopeKeys.size === 0) return false;
|
|
113
|
+
return scopeKeys.has(scopeKey);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
getScopeKeysForOwner(ownerKey: string): readonly string[] {
|
|
117
|
+
const scopeKeys = this.scopeKeysByOwnerKey.get(ownerKey);
|
|
118
|
+
return scopeKeys ? Array.from(scopeKeys) : [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getConnectionsForOwner(
|
|
122
|
+
ownerKey: string
|
|
123
|
+
): ReadonlySet<TConnection> | undefined {
|
|
124
|
+
return this.connectionsByOwnerKey.get(ownerKey);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getScopedConnectionCount(ownerKey: string): number {
|
|
128
|
+
return this.connectionsByOwnerKey.get(ownerKey)?.size ?? 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getConnectionsForClient(
|
|
132
|
+
clientId: string
|
|
133
|
+
): ReadonlySet<TConnection> | undefined {
|
|
134
|
+
return this.connectionsByClientId.get(clientId);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getConnectionCount(clientId: string): number {
|
|
138
|
+
return this.connectionsByClientId.get(clientId)?.size ?? 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getTotalConnections(): number {
|
|
142
|
+
let total = 0;
|
|
143
|
+
for (const conns of this.connectionsByOwnerKey.values()) {
|
|
144
|
+
total += conns.size;
|
|
145
|
+
}
|
|
146
|
+
return total;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
forEachConnectionInScopeKeys(
|
|
150
|
+
scopeKeys: Iterable<string>,
|
|
151
|
+
visitor: (connection: TConnection) => void,
|
|
152
|
+
options?: { excludeClientIds?: readonly string[] }
|
|
153
|
+
): void {
|
|
154
|
+
const targets = new Set<TConnection>();
|
|
155
|
+
for (const key of scopeKeys) {
|
|
156
|
+
const conns = this.connectionsByScopeKey.get(key);
|
|
157
|
+
if (!conns) continue;
|
|
158
|
+
for (const conn of conns) targets.add(conn);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const excludedClientIds = new Set(options?.excludeClientIds ?? []);
|
|
162
|
+
for (const conn of targets) {
|
|
163
|
+
if (!conn.isOpen) continue;
|
|
164
|
+
if (excludedClientIds.has(conn.clientId)) continue;
|
|
165
|
+
visitor(conn);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
forEachConnection(visitor: (connection: TConnection) => void): void {
|
|
170
|
+
for (const conns of this.connectionsByOwnerKey.values()) {
|
|
171
|
+
for (const conn of conns) {
|
|
172
|
+
if (!conn.isOpen) continue;
|
|
173
|
+
visitor(conn);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
closeOwnerConnections(
|
|
179
|
+
ownerKey: string,
|
|
180
|
+
code?: number,
|
|
181
|
+
reason?: string
|
|
182
|
+
): void {
|
|
183
|
+
const conns = this.connectionsByOwnerKey.get(ownerKey);
|
|
184
|
+
if (!conns) return;
|
|
185
|
+
|
|
186
|
+
for (const conn of Array.from(conns)) {
|
|
187
|
+
conn.close(code, reason);
|
|
188
|
+
this.unregister(conn);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.ensureHeartbeat();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
closeClientConnections(
|
|
195
|
+
clientId: string,
|
|
196
|
+
code?: number,
|
|
197
|
+
reason?: string
|
|
198
|
+
): void {
|
|
199
|
+
const conns = this.connectionsByClientId.get(clientId);
|
|
200
|
+
if (!conns) return;
|
|
201
|
+
|
|
202
|
+
for (const conn of Array.from(conns)) {
|
|
203
|
+
conn.close(code, reason);
|
|
204
|
+
this.unregister(conn);
|
|
205
|
+
}
|
|
206
|
+
this.ensureHeartbeat();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
closeAll(code?: number, reason?: string): void {
|
|
210
|
+
for (const conns of this.connectionsByOwnerKey.values()) {
|
|
211
|
+
for (const conn of Array.from(conns)) {
|
|
212
|
+
conn.close(code, reason);
|
|
213
|
+
this.unregister(conn);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
this.ensureHeartbeat();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private ensureHeartbeat(): void {
|
|
220
|
+
if (this.heartbeatIntervalMs <= 0) return;
|
|
221
|
+
|
|
222
|
+
const total = this.getTotalConnections();
|
|
223
|
+
if (total === 0) {
|
|
224
|
+
if (this.heartbeatTimer) {
|
|
225
|
+
clearInterval(this.heartbeatTimer);
|
|
226
|
+
this.heartbeatTimer = null;
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (this.heartbeatTimer) return;
|
|
232
|
+
this.heartbeatTimer = setInterval(() => {
|
|
233
|
+
this.sendHeartbeats();
|
|
234
|
+
}, this.heartbeatIntervalMs);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private sendHeartbeats(): void {
|
|
238
|
+
const closedConnections: TConnection[] = [];
|
|
239
|
+
for (const conns of this.connectionsByOwnerKey.values()) {
|
|
240
|
+
for (const conn of conns) {
|
|
241
|
+
if (!conn.isOpen) {
|
|
242
|
+
closedConnections.push(conn);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
conn.sendHeartbeat();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const conn of closedConnections) {
|
|
250
|
+
this.unregister(conn);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
this.ensureHeartbeat();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private unregister(connection: TConnection): void {
|
|
257
|
+
const ownerKey = connection.ownerKey;
|
|
258
|
+
|
|
259
|
+
const scopeKeys = this.scopeKeysByOwnerKey.get(ownerKey);
|
|
260
|
+
if (scopeKeys) {
|
|
261
|
+
for (const key of scopeKeys) {
|
|
262
|
+
const scopedConns = this.connectionsByScopeKey.get(key);
|
|
263
|
+
if (!scopedConns) continue;
|
|
264
|
+
scopedConns.delete(connection);
|
|
265
|
+
if (scopedConns.size === 0) this.connectionsByScopeKey.delete(key);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const ownerConns = this.connectionsByOwnerKey.get(ownerKey);
|
|
270
|
+
if (ownerConns) {
|
|
271
|
+
ownerConns.delete(connection);
|
|
272
|
+
if (ownerConns.size === 0) {
|
|
273
|
+
this.connectionsByOwnerKey.delete(ownerKey);
|
|
274
|
+
this.scopeKeysByOwnerKey.delete(ownerKey);
|
|
275
|
+
this.onOwnerDisconnected?.(ownerKey);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const clientConns = this.connectionsByClientId.get(connection.clientId);
|
|
280
|
+
if (!clientConns) return;
|
|
281
|
+
clientConns.delete(connection);
|
|
282
|
+
if (clientConns.size === 0) {
|
|
283
|
+
this.connectionsByClientId.delete(connection.clientId);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
type TableEntry = {
|
|
2
|
+
table: string;
|
|
3
|
+
dependsOn?: readonly string[];
|
|
4
|
+
};
|
|
5
|
+
export function registerTableOrThrow(
|
|
6
|
+
registeredTables: Set<string>,
|
|
7
|
+
table: string,
|
|
8
|
+
duplicateTableError: (table: string) => string
|
|
9
|
+
): void {
|
|
10
|
+
if (registeredTables.has(table)) throw new Error(duplicateTableError(table));
|
|
11
|
+
registeredTables.add(table);
|
|
12
|
+
}
|
|
13
|
+
export function createTableLookup<T extends TableEntry>(
|
|
14
|
+
handlers: readonly T[],
|
|
15
|
+
duplicateTableError: (table: string) => string
|
|
16
|
+
): ReadonlyMap<string, T> {
|
|
17
|
+
const byTable = new Map<string, T>();
|
|
18
|
+
for (const handler of handlers) {
|
|
19
|
+
if (byTable.has(handler.table)) {
|
|
20
|
+
throw new Error(duplicateTableError(handler.table));
|
|
21
|
+
}
|
|
22
|
+
byTable.set(handler.table, handler);
|
|
23
|
+
}
|
|
24
|
+
return byTable;
|
|
25
|
+
}
|
|
26
|
+
export function assertKnownTableDependencies<T extends TableEntry>(
|
|
27
|
+
handlers: readonly T[],
|
|
28
|
+
byTable: ReadonlyMap<string, T>,
|
|
29
|
+
unknownDependencyError: (table: string, dependency: string) => string
|
|
30
|
+
): void {
|
|
31
|
+
for (const handler of handlers) {
|
|
32
|
+
for (const dependency of handler.dependsOn ?? []) {
|
|
33
|
+
if (!byTable.has(dependency)) {
|
|
34
|
+
throw new Error(unknownDependencyError(handler.table, dependency));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function topologicallySortTablesByDependencies<T extends TableEntry>(
|
|
40
|
+
byTable: ReadonlyMap<string, T>,
|
|
41
|
+
targetTable?: string
|
|
42
|
+
): T[] {
|
|
43
|
+
const visited = new Set<string>();
|
|
44
|
+
const visiting = new Set<string>();
|
|
45
|
+
const sorted: T[] = [];
|
|
46
|
+
const visit = (table: string) => {
|
|
47
|
+
if (visited.has(table)) return;
|
|
48
|
+
if (visiting.has(table)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Circular dependency detected involving table "${table}"`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const handler = byTable.get(table);
|
|
54
|
+
if (!handler) throw new Error(`Unknown table: ${table}`);
|
|
55
|
+
visiting.add(table);
|
|
56
|
+
for (const dependency of handler.dependsOn ?? []) visit(dependency);
|
|
57
|
+
visiting.delete(table);
|
|
58
|
+
visited.add(table);
|
|
59
|
+
sorted.push(handler);
|
|
60
|
+
};
|
|
61
|
+
if (targetTable) {
|
|
62
|
+
visit(targetTable);
|
|
63
|
+
} else {
|
|
64
|
+
for (const table of byTable.keys()) visit(table);
|
|
65
|
+
}
|
|
66
|
+
return sorted;
|
|
67
|
+
}
|
package/src/utils/url.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const ABSOLUTE_URL_SCHEME_REGEX = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
2
|
+
|
|
3
|
+
const trimTrailingSlashes = (value: string): string =>
|
|
4
|
+
value.replace(/\/+$/, '');
|
|
5
|
+
|
|
6
|
+
export function normalizeSyncBaseUrl(url: string): string {
|
|
7
|
+
const trimmed = trimTrailingSlashes(url.trim());
|
|
8
|
+
if (!trimmed.endsWith('/sync')) return trimmed;
|
|
9
|
+
const baseUrl = trimmed.slice(0, -'/sync'.length);
|
|
10
|
+
return baseUrl.length > 0 ? baseUrl : '/';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveUrlFromBase(
|
|
14
|
+
baseUrl: string,
|
|
15
|
+
path: string,
|
|
16
|
+
origin?: string
|
|
17
|
+
): string {
|
|
18
|
+
const normalizedPath = path.replace(/^\/+/, '');
|
|
19
|
+
const normalizedBaseUrl = trimTrailingSlashes(baseUrl);
|
|
20
|
+
if (ABSOLUTE_URL_SCHEME_REGEX.test(baseUrl)) {
|
|
21
|
+
return new URL(
|
|
22
|
+
normalizedPath,
|
|
23
|
+
baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`
|
|
24
|
+
).toString();
|
|
25
|
+
}
|
|
26
|
+
if (origin)
|
|
27
|
+
return new URL(`${normalizedBaseUrl}/${normalizedPath}`, origin).toString();
|
|
28
|
+
return `${normalizedBaseUrl}/${normalizedPath}`;
|
|
29
|
+
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test';
|
|
2
|
-
import { gunzipSync } from 'node:zlib';
|
|
3
|
-
import { gzipBytes, gzipBytesToStream } from '../utils';
|
|
4
|
-
|
|
5
|
-
async function readStream(
|
|
6
|
-
stream: ReadableStream<Uint8Array>
|
|
7
|
-
): Promise<Uint8Array> {
|
|
8
|
-
const reader = stream.getReader();
|
|
9
|
-
const chunks: Uint8Array[] = [];
|
|
10
|
-
let total = 0;
|
|
11
|
-
|
|
12
|
-
while (true) {
|
|
13
|
-
const { done, value } = await reader.read();
|
|
14
|
-
if (done) break;
|
|
15
|
-
if (!value) continue;
|
|
16
|
-
chunks.push(value);
|
|
17
|
-
total += value.length;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (chunks.length === 0) return new Uint8Array();
|
|
21
|
-
if (chunks.length === 1) return chunks[0] ?? new Uint8Array();
|
|
22
|
-
|
|
23
|
-
const merged = new Uint8Array(total);
|
|
24
|
-
let offset = 0;
|
|
25
|
-
for (const chunk of chunks) {
|
|
26
|
-
merged.set(chunk, offset);
|
|
27
|
-
offset += chunk.length;
|
|
28
|
-
}
|
|
29
|
-
return merged;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe('gzipBytes', () => {
|
|
33
|
-
it('compresses bytes that can be decompressed via gunzip', async () => {
|
|
34
|
-
const payload = new TextEncoder().encode(
|
|
35
|
-
'syncular compression test '.repeat(64)
|
|
36
|
-
);
|
|
37
|
-
const compressed = await gzipBytes(payload);
|
|
38
|
-
const decompressed = new Uint8Array(gunzipSync(compressed));
|
|
39
|
-
expect(decompressed).toEqual(payload);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe('gzipBytesToStream', () => {
|
|
44
|
-
it('returns a gzip stream that round-trips', async () => {
|
|
45
|
-
const payload = new TextEncoder().encode('stream compression '.repeat(64));
|
|
46
|
-
const result = await gzipBytesToStream(payload);
|
|
47
|
-
const compressed = await readStream(result.stream);
|
|
48
|
-
const decompressed = new Uint8Array(gunzipSync(compressed));
|
|
49
|
-
expect(decompressed).toEqual(payload);
|
|
50
|
-
if (typeof result.byteLength === 'number') {
|
|
51
|
-
expect(result.byteLength).toBe(compressed.length);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
});
|