@absolutejs/sync 1.15.0 → 1.17.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/adapters/tanstack-db/index.js +45 -5
- package/dist/adapters/tanstack-db/index.js.map +5 -4
- package/dist/angular/index.js +45 -5
- package/dist/angular/index.js.map +5 -4
- package/dist/client/index.js +75 -17
- package/dist/client/index.js.map +8 -7
- package/dist/client/presence.d.ts +6 -0
- package/dist/client/syncClient.d.ts +8 -0
- package/dist/client/syncCollection.d.ts +6 -0
- package/dist/client/syncStore.d.ts +6 -0
- package/dist/engine/connection.d.ts +46 -3
- package/dist/engine/index.js +103 -21
- package/dist/engine/index.js.map +6 -5
- package/dist/engine/socket.d.ts +61 -7
- package/dist/engine/syncEngine.d.ts +30 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +165 -33
- package/dist/index.js.map +7 -6
- package/dist/react/index.js +45 -5
- package/dist/react/index.js.map +5 -4
- package/dist/serializer.d.ts +36 -0
- package/dist/svelte/index.js +45 -5
- package/dist/svelte/index.js.map +5 -4
- package/dist/testing.js +47 -11
- package/dist/testing.js.map +3 -3
- package/dist/vue/index.js +45 -5
- package/dist/vue/index.js.map +5 -4
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { RowKey } from '../engine/types';
|
|
2
2
|
import type { MutationStorage } from './syncCollection';
|
|
3
|
+
import { type FrameSerializer } from '../serializer';
|
|
3
4
|
export type SyncStoreStatus = 'connecting' | 'ready' | 'closed';
|
|
4
5
|
export type SyncStoreState<Row> = {
|
|
5
6
|
/** Visible rows: confirmed server state with pending optimistic edits applied. */
|
|
@@ -49,6 +50,11 @@ export type SyncStoreOptions<Row, M extends MutationMap> = {
|
|
|
49
50
|
/** Persist the pending-mutation queue across reloads (offline). */
|
|
50
51
|
storage?: MutationStorage;
|
|
51
52
|
onError?: (error: unknown) => void;
|
|
53
|
+
/**
|
|
54
|
+
* Wire-format serializer (1.16.0). Defaults to `jsonSerializer`. MUST
|
|
55
|
+
* match the server's `syncSocket` serializer.
|
|
56
|
+
*/
|
|
57
|
+
serializer?: FrameSerializer;
|
|
52
58
|
};
|
|
53
59
|
export type SyncStore<Row, M extends MutationMap> = {
|
|
54
60
|
get: () => SyncStoreState<Row>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PresenceHub, PresenceMember } from './presence';
|
|
2
2
|
import type { SyncEngine } from './syncEngine';
|
|
3
|
+
import type { FrameSerializer } from '../serializer';
|
|
3
4
|
/**
|
|
4
5
|
* Wire protocol for the sync-engine WebSocket. One connection multiplexes many
|
|
5
6
|
* collection subscriptions, each tagged with a client-chosen `id`.
|
|
@@ -80,16 +81,58 @@ export type SyncConnectionOptions = {
|
|
|
80
81
|
engine: SyncEngine;
|
|
81
82
|
/** Resolved auth context for this connection; passed to every subscribe. */
|
|
82
83
|
ctx: unknown;
|
|
83
|
-
/**
|
|
84
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Send a frame to the client (the transport serializes it). May return
|
|
86
|
+
* a number — by convention `-1` signals transport-layer backpressure (the
|
|
87
|
+
* value Bun's `ws.send()` returns when the WS buffer is full). The
|
|
88
|
+
* connection tracks consecutive `-1` returns and surfaces them via
|
|
89
|
+
* `connection.stats().slowSendsRecent`. Legacy `void`-returning sends
|
|
90
|
+
* keep working unchanged.
|
|
91
|
+
*
|
|
92
|
+
* NOTE: 1.16.0 — `send` receives the typed `ServerFrame`. The connection
|
|
93
|
+
* does NOT pre-serialize; the WS adapter (`syncSocket`) wraps `send` to
|
|
94
|
+
* call `serializer.encodeServer(frame)` before `ws.send(...)`. This keeps
|
|
95
|
+
* the connection layer transport-agnostic.
|
|
96
|
+
*/
|
|
97
|
+
send: (frame: ServerFrame) => void | number;
|
|
85
98
|
/** Optional presence hub; enables the `presence-*` frames (see createPresenceHub). */
|
|
86
99
|
presence?: PresenceHub;
|
|
100
|
+
/**
|
|
101
|
+
* Wire-format serializer (1.16.0). Defaults to `jsonSerializer` —
|
|
102
|
+
* the historical JSON-over-WS behavior. Both ends of the connection
|
|
103
|
+
* MUST use the same serializer; pair this option with the matching
|
|
104
|
+
* client-side `serializer` to opt into binary frames.
|
|
105
|
+
*/
|
|
106
|
+
serializer?: FrameSerializer;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Connection-level operational counters surfaced via {@link SyncConnection.stats}.
|
|
110
|
+
* Pair with the WS adapter's own backpressure signal for end-to-end slow-client
|
|
111
|
+
* detection.
|
|
112
|
+
*/
|
|
113
|
+
export type SyncConnectionStats = {
|
|
114
|
+
/** Active subscriptions on this connection. */
|
|
115
|
+
subscriptionCount: number;
|
|
116
|
+
/** Active presence-room memberships on this connection. */
|
|
117
|
+
presenceRoomCount: number;
|
|
118
|
+
/** Frames successfully sent (non-backpressure return) since the connection opened. */
|
|
119
|
+
framesSent: number;
|
|
120
|
+
/** Consecutive `-1` (backpressure) returns from `send` since the last successful send. */
|
|
121
|
+
slowSendsRecent: number;
|
|
87
122
|
};
|
|
88
123
|
export type SyncConnection = {
|
|
89
124
|
/** Handle one client frame (a parsed object or a raw JSON string). */
|
|
90
125
|
handle: (raw: unknown) => Promise<void>;
|
|
91
126
|
/** Tear down every subscription on this connection (call on socket close). */
|
|
92
127
|
close: () => void;
|
|
128
|
+
/**
|
|
129
|
+
* Point-in-time connection counters — subscription count, frames sent, and
|
|
130
|
+
* how many consecutive `send` calls came back with the transport's backpressure
|
|
131
|
+
* signal. Cheap; safe to call from a metering loop.
|
|
132
|
+
*
|
|
133
|
+
* Added in 1.14.0.
|
|
134
|
+
*/
|
|
135
|
+
stats: () => SyncConnectionStats;
|
|
93
136
|
};
|
|
94
137
|
/**
|
|
95
138
|
* The per-connection protocol handler — transport-agnostic glue between a single
|
|
@@ -100,4 +143,4 @@ export type SyncConnection = {
|
|
|
100
143
|
* Pure (no WebSocket import) so it can be unit-tested with a fake `send`; the
|
|
101
144
|
* Elysia `syncSocket` plugin is the thin adapter that feeds it socket events.
|
|
102
145
|
*/
|
|
103
|
-
export declare const createSyncConnection: ({ engine, ctx, send, presence }: SyncConnectionOptions) => SyncConnection;
|
|
146
|
+
export declare const createSyncConnection: ({ engine, ctx, send: rawSend, presence, serializer }: SyncConnectionOptions) => SyncConnection;
|
package/dist/engine/index.js
CHANGED
|
@@ -1356,6 +1356,32 @@ class UnauthorizedError extends Error {
|
|
|
1356
1356
|
}
|
|
1357
1357
|
}
|
|
1358
1358
|
|
|
1359
|
+
class AbortError extends Error {
|
|
1360
|
+
constructor(reason) {
|
|
1361
|
+
super(reason ?? "Aborted");
|
|
1362
|
+
this.name = "AbortError";
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
var checkAborted = (signal) => {
|
|
1366
|
+
if (signal?.aborted) {
|
|
1367
|
+
throw new AbortError(signal.reason instanceof Error ? signal.reason.message : typeof signal.reason === "string" ? signal.reason : "Aborted");
|
|
1368
|
+
}
|
|
1369
|
+
};
|
|
1370
|
+
var linkAbortToUnsubscribe = (signal, unsubscribe) => {
|
|
1371
|
+
if (signal === undefined)
|
|
1372
|
+
return;
|
|
1373
|
+
if (signal.aborted) {
|
|
1374
|
+
unsubscribe();
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
const handler = () => {
|
|
1378
|
+
try {
|
|
1379
|
+
unsubscribe();
|
|
1380
|
+
} catch {}
|
|
1381
|
+
};
|
|
1382
|
+
signal.addEventListener("abort", handler, { once: true });
|
|
1383
|
+
};
|
|
1384
|
+
|
|
1359
1385
|
class SchemaError extends Error {
|
|
1360
1386
|
constructor(table, fieldName) {
|
|
1361
1387
|
super(`Schema violation on "${table}": invalid field "${fieldName}"`);
|
|
@@ -2218,29 +2244,35 @@ var createSyncEngine = (options = {}) => {
|
|
|
2218
2244
|
registerSearch: (collection) => {
|
|
2219
2245
|
registry.set(collection.name, collection);
|
|
2220
2246
|
},
|
|
2221
|
-
subscribe: async ({ collection, params, ctx, onDiff, since }) => {
|
|
2247
|
+
subscribe: async ({ collection, params, ctx, onDiff, since, signal }) => {
|
|
2248
|
+
checkAborted(signal);
|
|
2222
2249
|
const registered = registry.get(collection);
|
|
2223
2250
|
if (registered === undefined) {
|
|
2224
2251
|
throw new Error(`Unknown collection "${collection}"`);
|
|
2225
2252
|
}
|
|
2226
2253
|
const typedOnDiff = onDiff;
|
|
2227
2254
|
const subscribeSet = subsFor(collection);
|
|
2255
|
+
const wrapReturn = (sub) => {
|
|
2256
|
+
checkAborted(signal);
|
|
2257
|
+
linkAbortToUnsubscribe(signal, sub.unsubscribe);
|
|
2258
|
+
return sub;
|
|
2259
|
+
};
|
|
2228
2260
|
const registeredKind = registered.kind;
|
|
2229
2261
|
if (registeredKind === "join") {
|
|
2230
2262
|
const joined = await subscribeJoin(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2231
|
-
return joined;
|
|
2263
|
+
return wrapReturn(joined);
|
|
2232
2264
|
}
|
|
2233
2265
|
if (registeredKind === "graph") {
|
|
2234
2266
|
const graphed = await subscribeGraph(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2235
|
-
return graphed;
|
|
2267
|
+
return wrapReturn(graphed);
|
|
2236
2268
|
}
|
|
2237
2269
|
if (registeredKind === "reactive") {
|
|
2238
2270
|
const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2239
|
-
return reactived;
|
|
2271
|
+
return wrapReturn(reactived);
|
|
2240
2272
|
}
|
|
2241
2273
|
if (registeredKind === "search") {
|
|
2242
2274
|
const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
2243
|
-
return searched;
|
|
2275
|
+
return wrapReturn(searched);
|
|
2244
2276
|
}
|
|
2245
2277
|
const definition = registered;
|
|
2246
2278
|
if (definition.authorize !== undefined) {
|
|
@@ -2282,31 +2314,35 @@ var createSyncEngine = (options = {}) => {
|
|
|
2282
2314
|
subscribeSet.delete(subscription);
|
|
2283
2315
|
};
|
|
2284
2316
|
if (resuming) {
|
|
2285
|
-
return {
|
|
2317
|
+
return wrapReturn({
|
|
2286
2318
|
initial: [],
|
|
2287
2319
|
catchup: buildCatchup(since, tables, key, boundMatch),
|
|
2288
2320
|
version: atVersion,
|
|
2289
2321
|
unsubscribe
|
|
2290
|
-
};
|
|
2322
|
+
});
|
|
2291
2323
|
}
|
|
2292
|
-
return {
|
|
2324
|
+
return wrapReturn({
|
|
2293
2325
|
initial: view.rows(),
|
|
2294
2326
|
version: atVersion,
|
|
2295
2327
|
unsubscribe
|
|
2296
|
-
};
|
|
2328
|
+
});
|
|
2297
2329
|
},
|
|
2298
|
-
hydrate: async (collection, params, ctx) => {
|
|
2330
|
+
hydrate: async (collection, params, ctx, options2) => {
|
|
2331
|
+
const signal = options2?.signal;
|
|
2332
|
+
checkAborted(signal);
|
|
2299
2333
|
const definition = registry.get(collection);
|
|
2300
2334
|
if (definition === undefined) {
|
|
2301
2335
|
throw new Error(`Unknown collection "${collection}"`);
|
|
2302
2336
|
}
|
|
2303
2337
|
if (definition.authorize !== undefined) {
|
|
2304
2338
|
const allowed = await definition.authorize(params, ctx);
|
|
2339
|
+
checkAborted(signal);
|
|
2305
2340
|
if (!allowed) {
|
|
2306
2341
|
throw new UnauthorizedError(`hydrate collection "${collection}"`);
|
|
2307
2342
|
}
|
|
2308
2343
|
}
|
|
2309
2344
|
const raw = [...await definition.hydrate(params, ctx)];
|
|
2345
|
+
checkAborted(signal);
|
|
2310
2346
|
const tables = definition.tables ?? [collection];
|
|
2311
2347
|
const scopedTable = tables.length === 1 ? tables[0] : undefined;
|
|
2312
2348
|
const rows = scopedTable !== undefined ? raw.map((row) => migrateRow(scopedTable, row)) : raw;
|
|
@@ -2953,15 +2989,43 @@ var mutateRoute = (engine, mutation, resolveContext = emptyContext) => {
|
|
|
2953
2989
|
return result;
|
|
2954
2990
|
};
|
|
2955
2991
|
};
|
|
2992
|
+
// src/serializer.ts
|
|
2993
|
+
var jsonSerializer = {
|
|
2994
|
+
decode: (raw) => {
|
|
2995
|
+
if (typeof raw === "string") {
|
|
2996
|
+
try {
|
|
2997
|
+
return JSON.parse(raw);
|
|
2998
|
+
} catch {
|
|
2999
|
+
return null;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
if (raw instanceof Uint8Array) {
|
|
3003
|
+
try {
|
|
3004
|
+
return JSON.parse(new TextDecoder().decode(raw));
|
|
3005
|
+
} catch {
|
|
3006
|
+
return null;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
if (raw instanceof ArrayBuffer) {
|
|
3010
|
+
try {
|
|
3011
|
+
return JSON.parse(new TextDecoder().decode(new Uint8Array(raw)));
|
|
3012
|
+
} catch {
|
|
3013
|
+
return null;
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
return raw;
|
|
3017
|
+
},
|
|
3018
|
+
encodeClient: (frame) => JSON.stringify(frame),
|
|
3019
|
+
encodeServer: (frame) => JSON.stringify(frame)
|
|
3020
|
+
};
|
|
3021
|
+
|
|
2956
3022
|
// src/engine/connection.ts
|
|
2957
|
-
var parseFrame = (raw) => {
|
|
3023
|
+
var parseFrame = (raw, serializer) => {
|
|
2958
3024
|
let value = raw;
|
|
2959
|
-
if (typeof value === "string") {
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
} catch {
|
|
3025
|
+
if (typeof value === "string" || value instanceof Uint8Array || value instanceof ArrayBuffer) {
|
|
3026
|
+
value = serializer.decode(raw);
|
|
3027
|
+
if (value === null)
|
|
2963
3028
|
return;
|
|
2964
|
-
}
|
|
2965
3029
|
}
|
|
2966
3030
|
if (typeof value !== "object" || value === null) {
|
|
2967
3031
|
return;
|
|
@@ -3006,11 +3070,23 @@ var parseFrame = (raw) => {
|
|
|
3006
3070
|
var createSyncConnection = ({
|
|
3007
3071
|
engine,
|
|
3008
3072
|
ctx,
|
|
3009
|
-
send,
|
|
3010
|
-
presence
|
|
3073
|
+
send: rawSend,
|
|
3074
|
+
presence,
|
|
3075
|
+
serializer = jsonSerializer
|
|
3011
3076
|
}) => {
|
|
3012
3077
|
const subscriptions = new Map;
|
|
3013
3078
|
const presenceRooms = new Map;
|
|
3079
|
+
let framesSent = 0;
|
|
3080
|
+
let slowSendsRecent = 0;
|
|
3081
|
+
const send = (frame) => {
|
|
3082
|
+
const ret = rawSend(frame);
|
|
3083
|
+
if (ret === -1) {
|
|
3084
|
+
slowSendsRecent += 1;
|
|
3085
|
+
} else {
|
|
3086
|
+
framesSent += 1;
|
|
3087
|
+
slowSendsRecent = 0;
|
|
3088
|
+
}
|
|
3089
|
+
};
|
|
3014
3090
|
let pending = [];
|
|
3015
3091
|
let pendingVersion;
|
|
3016
3092
|
let flushScheduled = false;
|
|
@@ -3055,7 +3131,7 @@ var createSyncConnection = ({
|
|
|
3055
3131
|
scheduleFlush();
|
|
3056
3132
|
};
|
|
3057
3133
|
const handle = async (raw) => {
|
|
3058
|
-
const frame = parseFrame(raw);
|
|
3134
|
+
const frame = parseFrame(raw, serializer);
|
|
3059
3135
|
if (frame === undefined) {
|
|
3060
3136
|
send({ type: "error", message: "Malformed sync frame" });
|
|
3061
3137
|
return;
|
|
@@ -3172,7 +3248,13 @@ var createSyncConnection = ({
|
|
|
3172
3248
|
}
|
|
3173
3249
|
presenceRooms.clear();
|
|
3174
3250
|
};
|
|
3175
|
-
|
|
3251
|
+
const stats = () => ({
|
|
3252
|
+
framesSent,
|
|
3253
|
+
presenceRoomCount: presenceRooms.size,
|
|
3254
|
+
slowSendsRecent,
|
|
3255
|
+
subscriptionCount: subscriptions.size
|
|
3256
|
+
});
|
|
3257
|
+
return { close, handle, stats };
|
|
3176
3258
|
};
|
|
3177
3259
|
export {
|
|
3178
3260
|
syncCdc,
|
|
@@ -3222,5 +3304,5 @@ export {
|
|
|
3222
3304
|
CdcConsumerSlowError
|
|
3223
3305
|
};
|
|
3224
3306
|
|
|
3225
|
-
//# debugId=
|
|
3307
|
+
//# debugId=2B8B686DC5E953C364756E2164756E21
|
|
3226
3308
|
//# sourceMappingURL=index.js.map
|