@korajs/sync 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/chunk-TU345YUC.js +23 -0
- package/dist/chunk-TU345YUC.js.map +1 -0
- package/dist/index.cjs +1720 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +505 -0
- package/dist/index.d.ts +505 -0
- package/dist/index.js +1659 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.cjs +146 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +68 -0
- package/dist/internal.d.ts +68 -0
- package/dist/internal.js +102 -0
- package/dist/internal.js.map +1 -0
- package/dist/transport-B5EFsr5F.d.cts +215 -0
- package/dist/transport-B5EFsr5F.d.ts +215 -0
- package/package.json +56 -0
package/dist/internal.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MemoryQueueStorage
|
|
3
|
+
} from "./chunk-TU345YUC.js";
|
|
4
|
+
|
|
5
|
+
// src/transport/memory-transport.ts
|
|
6
|
+
import { SyncError } from "@korajs/core";
|
|
7
|
+
var MemoryTransport = class {
|
|
8
|
+
connected = false;
|
|
9
|
+
messageHandler = null;
|
|
10
|
+
closeHandler = null;
|
|
11
|
+
errorHandler = null;
|
|
12
|
+
peer = null;
|
|
13
|
+
sentMessages = [];
|
|
14
|
+
/** Link this transport to its peer (the other end of the connection) */
|
|
15
|
+
linkPeer(peer) {
|
|
16
|
+
this.peer = peer;
|
|
17
|
+
}
|
|
18
|
+
async connect(_url, _options) {
|
|
19
|
+
if (!this.peer) {
|
|
20
|
+
throw new SyncError("MemoryTransport has no linked peer", {
|
|
21
|
+
reason: "not-linked"
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
this.connected = true;
|
|
25
|
+
this.peer.connected = true;
|
|
26
|
+
}
|
|
27
|
+
async disconnect() {
|
|
28
|
+
if (!this.connected) return;
|
|
29
|
+
this.connected = false;
|
|
30
|
+
if (this.peer?.connected) {
|
|
31
|
+
this.peer.connected = false;
|
|
32
|
+
this.peer.closeHandler?.("peer disconnected");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
send(message) {
|
|
36
|
+
if (!this.connected) {
|
|
37
|
+
throw new SyncError("Cannot send message: transport is not connected", {
|
|
38
|
+
messageType: message.type
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
this.sentMessages.push(message);
|
|
42
|
+
this.peer?.messageHandler?.(message);
|
|
43
|
+
}
|
|
44
|
+
onMessage(handler) {
|
|
45
|
+
this.messageHandler = handler;
|
|
46
|
+
}
|
|
47
|
+
onClose(handler) {
|
|
48
|
+
this.closeHandler = handler;
|
|
49
|
+
}
|
|
50
|
+
onError(handler) {
|
|
51
|
+
this.errorHandler = handler;
|
|
52
|
+
}
|
|
53
|
+
isConnected() {
|
|
54
|
+
return this.connected;
|
|
55
|
+
}
|
|
56
|
+
// --- Testing helpers ---
|
|
57
|
+
/**
|
|
58
|
+
* Simulate an incoming message (as if received from peer).
|
|
59
|
+
* Useful for testing without a linked peer.
|
|
60
|
+
*/
|
|
61
|
+
simulateIncoming(message) {
|
|
62
|
+
this.messageHandler?.(message);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Simulate a disconnect from the remote side.
|
|
66
|
+
*/
|
|
67
|
+
simulateDisconnect(reason) {
|
|
68
|
+
this.connected = false;
|
|
69
|
+
this.closeHandler?.(reason);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Simulate a transport error.
|
|
73
|
+
*/
|
|
74
|
+
simulateError(error) {
|
|
75
|
+
this.errorHandler?.(error);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get all messages sent through this transport.
|
|
79
|
+
*/
|
|
80
|
+
getSentMessages() {
|
|
81
|
+
return [...this.sentMessages];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Clear the sent messages history.
|
|
85
|
+
*/
|
|
86
|
+
clearSentMessages() {
|
|
87
|
+
this.sentMessages.length = 0;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
function createMemoryTransportPair() {
|
|
91
|
+
const client = new MemoryTransport();
|
|
92
|
+
const server = new MemoryTransport();
|
|
93
|
+
client.linkPeer(server);
|
|
94
|
+
server.linkPeer(client);
|
|
95
|
+
return { client, server };
|
|
96
|
+
}
|
|
97
|
+
export {
|
|
98
|
+
MemoryQueueStorage,
|
|
99
|
+
MemoryTransport,
|
|
100
|
+
createMemoryTransportPair
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=internal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/transport/memory-transport.ts"],"sourcesContent":["import { SyncError } from '@korajs/core'\nimport type { SyncMessage } from '../protocol/messages'\nimport type {\n\tSyncTransport,\n\tTransportCloseHandler,\n\tTransportErrorHandler,\n\tTransportMessageHandler,\n\tTransportOptions,\n} from './transport'\n\n/**\n * In-memory transport for testing. Provides instant, synchronous message delivery\n * between a linked pair of transports (simulating client ↔ server).\n */\nexport class MemoryTransport implements SyncTransport {\n\tprivate connected = false\n\tprivate messageHandler: TransportMessageHandler | null = null\n\tprivate closeHandler: TransportCloseHandler | null = null\n\tprivate errorHandler: TransportErrorHandler | null = null\n\tprivate peer: MemoryTransport | null = null\n\tprivate readonly sentMessages: SyncMessage[] = []\n\n\t/** Link this transport to its peer (the other end of the connection) */\n\tlinkPeer(peer: MemoryTransport): void {\n\t\tthis.peer = peer\n\t}\n\n\tasync connect(_url: string, _options?: TransportOptions): Promise<void> {\n\t\tif (!this.peer) {\n\t\t\tthrow new SyncError('MemoryTransport has no linked peer', {\n\t\t\t\treason: 'not-linked',\n\t\t\t})\n\t\t}\n\t\tthis.connected = true\n\t\tthis.peer.connected = true\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tif (!this.connected) return\n\t\tthis.connected = false\n\t\t// Notify peer that we disconnected\n\t\tif (this.peer?.connected) {\n\t\t\tthis.peer.connected = false\n\t\t\tthis.peer.closeHandler?.('peer disconnected')\n\t\t}\n\t}\n\n\tsend(message: SyncMessage): void {\n\t\tif (!this.connected) {\n\t\t\tthrow new SyncError('Cannot send message: transport is not connected', {\n\t\t\t\tmessageType: message.type,\n\t\t\t})\n\t\t}\n\t\tthis.sentMessages.push(message)\n\t\t// Deliver to peer's message handler\n\t\tthis.peer?.messageHandler?.(message)\n\t}\n\n\tonMessage(handler: TransportMessageHandler): void {\n\t\tthis.messageHandler = handler\n\t}\n\n\tonClose(handler: TransportCloseHandler): void {\n\t\tthis.closeHandler = handler\n\t}\n\n\tonError(handler: TransportErrorHandler): void {\n\t\tthis.errorHandler = handler\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.connected\n\t}\n\n\t// --- Testing helpers ---\n\n\t/**\n\t * Simulate an incoming message (as if received from peer).\n\t * Useful for testing without a linked peer.\n\t */\n\tsimulateIncoming(message: SyncMessage): void {\n\t\tthis.messageHandler?.(message)\n\t}\n\n\t/**\n\t * Simulate a disconnect from the remote side.\n\t */\n\tsimulateDisconnect(reason: string): void {\n\t\tthis.connected = false\n\t\tthis.closeHandler?.(reason)\n\t}\n\n\t/**\n\t * Simulate a transport error.\n\t */\n\tsimulateError(error: Error): void {\n\t\tthis.errorHandler?.(error)\n\t}\n\n\t/**\n\t * Get all messages sent through this transport.\n\t */\n\tgetSentMessages(): SyncMessage[] {\n\t\treturn [...this.sentMessages]\n\t}\n\n\t/**\n\t * Clear the sent messages history.\n\t */\n\tclearSentMessages(): void {\n\t\tthis.sentMessages.length = 0\n\t}\n}\n\n/**\n * Create a linked pair of memory transports for testing.\n * Messages sent on `client` arrive at `server` and vice versa.\n */\nexport function createMemoryTransportPair(): { client: MemoryTransport; server: MemoryTransport } {\n\tconst client = new MemoryTransport()\n\tconst server = new MemoryTransport()\n\tclient.linkPeer(server)\n\tserver.linkPeer(client)\n\treturn { client, server }\n}\n"],"mappings":";;;;;AAAA,SAAS,iBAAiB;AAcnB,IAAM,kBAAN,MAA+C;AAAA,EAC7C,YAAY;AAAA,EACZ,iBAAiD;AAAA,EACjD,eAA6C;AAAA,EAC7C,eAA6C;AAAA,EAC7C,OAA+B;AAAA,EACtB,eAA8B,CAAC;AAAA;AAAA,EAGhD,SAAS,MAA6B;AACrC,SAAK,OAAO;AAAA,EACb;AAAA,EAEA,MAAM,QAAQ,MAAc,UAA4C;AACvE,QAAI,CAAC,KAAK,MAAM;AACf,YAAM,IAAI,UAAU,sCAAsC;AAAA,QACzD,QAAQ;AAAA,MACT,CAAC;AAAA,IACF;AACA,SAAK,YAAY;AACjB,SAAK,KAAK,YAAY;AAAA,EACvB;AAAA,EAEA,MAAM,aAA4B;AACjC,QAAI,CAAC,KAAK,UAAW;AACrB,SAAK,YAAY;AAEjB,QAAI,KAAK,MAAM,WAAW;AACzB,WAAK,KAAK,YAAY;AACtB,WAAK,KAAK,eAAe,mBAAmB;AAAA,IAC7C;AAAA,EACD;AAAA,EAEA,KAAK,SAA4B;AAChC,QAAI,CAAC,KAAK,WAAW;AACpB,YAAM,IAAI,UAAU,mDAAmD;AAAA,QACtE,aAAa,QAAQ;AAAA,MACtB,CAAC;AAAA,IACF;AACA,SAAK,aAAa,KAAK,OAAO;AAE9B,SAAK,MAAM,iBAAiB,OAAO;AAAA,EACpC;AAAA,EAEA,UAAU,SAAwC;AACjD,SAAK,iBAAiB;AAAA,EACvB;AAAA,EAEA,QAAQ,SAAsC;AAC7C,SAAK,eAAe;AAAA,EACrB;AAAA,EAEA,QAAQ,SAAsC;AAC7C,SAAK,eAAe;AAAA,EACrB;AAAA,EAEA,cAAuB;AACtB,WAAO,KAAK;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,iBAAiB,SAA4B;AAC5C,SAAK,iBAAiB,OAAO;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAmB,QAAsB;AACxC,SAAK,YAAY;AACjB,SAAK,eAAe,MAAM;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc,OAAoB;AACjC,SAAK,eAAe,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,kBAAiC;AAChC,WAAO,CAAC,GAAG,KAAK,YAAY;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA,EAKA,oBAA0B;AACzB,SAAK,aAAa,SAAS;AAAA,EAC5B;AACD;AAMO,SAAS,4BAAkF;AACjG,QAAM,SAAS,IAAI,gBAAgB;AACnC,QAAM,SAAS,IAAI,gBAAgB;AACnC,SAAO,SAAS,MAAM;AACtB,SAAO,SAAS,MAAM;AACtB,SAAO,EAAE,QAAQ,OAAO;AACzB;","names":[]}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Operation, OperationType, HLCTimestamp } from '@korajs/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Internal sync engine states. Used for state machine transitions.
|
|
5
|
+
*/
|
|
6
|
+
declare const SYNC_STATES: readonly ["disconnected", "connecting", "handshaking", "syncing", "streaming", "error"];
|
|
7
|
+
type SyncState = (typeof SYNC_STATES)[number];
|
|
8
|
+
/**
|
|
9
|
+
* Developer-facing sync status. Simplified view of the internal state.
|
|
10
|
+
*/
|
|
11
|
+
declare const SYNC_STATUSES: readonly ["connected", "syncing", "synced", "offline", "error"];
|
|
12
|
+
type SyncStatus = (typeof SYNC_STATUSES)[number];
|
|
13
|
+
/**
|
|
14
|
+
* Sync status information exposed to developers.
|
|
15
|
+
*/
|
|
16
|
+
interface SyncStatusInfo {
|
|
17
|
+
/** Current developer-facing status */
|
|
18
|
+
status: SyncStatus;
|
|
19
|
+
/** Number of operations waiting to be sent */
|
|
20
|
+
pendingOperations: number;
|
|
21
|
+
/** Timestamp of last successful sync (null if never synced) */
|
|
22
|
+
lastSyncedAt: number | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Sync configuration provided by the developer.
|
|
26
|
+
*/
|
|
27
|
+
interface SyncConfig {
|
|
28
|
+
/** WebSocket or HTTP URL for the sync server */
|
|
29
|
+
url: string;
|
|
30
|
+
/** Transport type to use. Defaults to 'websocket'. */
|
|
31
|
+
transport?: 'websocket' | 'http';
|
|
32
|
+
/** Auth provider function. Called before each connection attempt. */
|
|
33
|
+
auth?: () => Promise<{
|
|
34
|
+
token: string;
|
|
35
|
+
}>;
|
|
36
|
+
/** Sync scopes per collection. Limits which records sync to this client. */
|
|
37
|
+
scopes?: Record<string, (ctx: SyncScopeContext) => Record<string, unknown>>;
|
|
38
|
+
/** Number of operations per batch. Defaults to 100. */
|
|
39
|
+
batchSize?: number;
|
|
40
|
+
/** Initial reconnection delay in ms. Defaults to 1000. */
|
|
41
|
+
reconnectInterval?: number;
|
|
42
|
+
/** Maximum reconnection delay in ms. Defaults to 30000. */
|
|
43
|
+
maxReconnectInterval?: number;
|
|
44
|
+
/** Schema version of this client. */
|
|
45
|
+
schemaVersion?: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Context passed to sync scope functions.
|
|
49
|
+
*/
|
|
50
|
+
interface SyncScopeContext {
|
|
51
|
+
userId?: string;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Interface for persisting the outbound operation queue.
|
|
56
|
+
* Operations must survive page refreshes and be sent when connection is re-established.
|
|
57
|
+
*/
|
|
58
|
+
interface QueueStorage {
|
|
59
|
+
/** Load all queued operations from persistent storage */
|
|
60
|
+
load(): Promise<Operation[]>;
|
|
61
|
+
/** Persist an operation to the queue */
|
|
62
|
+
enqueue(op: Operation): Promise<void>;
|
|
63
|
+
/** Remove acknowledged operations by their IDs */
|
|
64
|
+
dequeue(ids: string[]): Promise<void>;
|
|
65
|
+
/** Return number of operations in storage */
|
|
66
|
+
count(): Promise<number>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type WireFormat = 'json' | 'protobuf';
|
|
70
|
+
/**
|
|
71
|
+
* Wire-format operation. Plain object (no Map) for JSON serialization.
|
|
72
|
+
* Maps 1:1 with Operation, but uses Record instead of Map for version vectors.
|
|
73
|
+
*/
|
|
74
|
+
interface SerializedOperation {
|
|
75
|
+
id: string;
|
|
76
|
+
nodeId: string;
|
|
77
|
+
type: OperationType;
|
|
78
|
+
collection: string;
|
|
79
|
+
recordId: string;
|
|
80
|
+
data: Record<string, unknown> | null;
|
|
81
|
+
previousData: Record<string, unknown> | null;
|
|
82
|
+
timestamp: HLCTimestamp;
|
|
83
|
+
sequenceNumber: number;
|
|
84
|
+
causalDeps: string[];
|
|
85
|
+
schemaVersion: number;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Handshake message sent by client to initiate sync.
|
|
89
|
+
*/
|
|
90
|
+
interface HandshakeMessage {
|
|
91
|
+
type: 'handshake';
|
|
92
|
+
messageId: string;
|
|
93
|
+
nodeId: string;
|
|
94
|
+
/** Version vector as plain object (nodeId -> sequence number) */
|
|
95
|
+
versionVector: Record<string, number>;
|
|
96
|
+
schemaVersion: number;
|
|
97
|
+
authToken?: string;
|
|
98
|
+
supportedWireFormats?: WireFormat[];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Server response to a handshake.
|
|
102
|
+
*/
|
|
103
|
+
interface HandshakeResponseMessage {
|
|
104
|
+
type: 'handshake-response';
|
|
105
|
+
messageId: string;
|
|
106
|
+
nodeId: string;
|
|
107
|
+
versionVector: Record<string, number>;
|
|
108
|
+
schemaVersion: number;
|
|
109
|
+
accepted: boolean;
|
|
110
|
+
rejectReason?: string;
|
|
111
|
+
selectedWireFormat?: WireFormat;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Batch of operations sent during delta exchange or streaming.
|
|
115
|
+
*/
|
|
116
|
+
interface OperationBatchMessage {
|
|
117
|
+
type: 'operation-batch';
|
|
118
|
+
messageId: string;
|
|
119
|
+
operations: SerializedOperation[];
|
|
120
|
+
/** True if this is the last batch in the delta exchange phase */
|
|
121
|
+
isFinal: boolean;
|
|
122
|
+
/** Index of this batch (0-based) for ordering */
|
|
123
|
+
batchIndex: number;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Acknowledgment of a received message.
|
|
127
|
+
*/
|
|
128
|
+
interface AcknowledgmentMessage {
|
|
129
|
+
type: 'acknowledgment';
|
|
130
|
+
messageId: string;
|
|
131
|
+
acknowledgedMessageId: string;
|
|
132
|
+
lastSequenceNumber: number;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Error message from the server or client.
|
|
136
|
+
*/
|
|
137
|
+
interface ErrorMessage {
|
|
138
|
+
type: 'error';
|
|
139
|
+
messageId: string;
|
|
140
|
+
code: string;
|
|
141
|
+
message: string;
|
|
142
|
+
retriable: boolean;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Union of all sync protocol messages.
|
|
146
|
+
*/
|
|
147
|
+
type SyncMessage = HandshakeMessage | HandshakeResponseMessage | OperationBatchMessage | AcknowledgmentMessage | ErrorMessage;
|
|
148
|
+
/**
|
|
149
|
+
* Check if an unknown value is a valid SyncMessage.
|
|
150
|
+
*/
|
|
151
|
+
declare function isSyncMessage(value: unknown): value is SyncMessage;
|
|
152
|
+
/**
|
|
153
|
+
* Check if a value is a HandshakeMessage.
|
|
154
|
+
*/
|
|
155
|
+
declare function isHandshakeMessage(value: unknown): value is HandshakeMessage;
|
|
156
|
+
/**
|
|
157
|
+
* Check if a value is a HandshakeResponseMessage.
|
|
158
|
+
*/
|
|
159
|
+
declare function isHandshakeResponseMessage(value: unknown): value is HandshakeResponseMessage;
|
|
160
|
+
/**
|
|
161
|
+
* Check if a value is an OperationBatchMessage.
|
|
162
|
+
*/
|
|
163
|
+
declare function isOperationBatchMessage(value: unknown): value is OperationBatchMessage;
|
|
164
|
+
/**
|
|
165
|
+
* Check if a value is an AcknowledgmentMessage.
|
|
166
|
+
*/
|
|
167
|
+
declare function isAcknowledgmentMessage(value: unknown): value is AcknowledgmentMessage;
|
|
168
|
+
/**
|
|
169
|
+
* Check if a value is an ErrorMessage.
|
|
170
|
+
*/
|
|
171
|
+
declare function isErrorMessage(value: unknown): value is ErrorMessage;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Options for transport connection.
|
|
175
|
+
*/
|
|
176
|
+
interface TransportOptions {
|
|
177
|
+
/** Authentication token sent during connection */
|
|
178
|
+
authToken?: string;
|
|
179
|
+
/** Additional headers (for HTTP-based transports) */
|
|
180
|
+
headers?: Record<string, string>;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Transport-level error handler.
|
|
184
|
+
*/
|
|
185
|
+
type TransportErrorHandler = (error: Error) => void;
|
|
186
|
+
/**
|
|
187
|
+
* Transport-level close handler.
|
|
188
|
+
*/
|
|
189
|
+
type TransportCloseHandler = (reason: string) => void;
|
|
190
|
+
/**
|
|
191
|
+
* Transport-level message handler.
|
|
192
|
+
*/
|
|
193
|
+
type TransportMessageHandler = (message: SyncMessage) => void;
|
|
194
|
+
/**
|
|
195
|
+
* Abstract transport interface for sync communication.
|
|
196
|
+
* Implementations can use WebSocket, HTTP, Bluetooth, or any other transport.
|
|
197
|
+
*/
|
|
198
|
+
interface SyncTransport {
|
|
199
|
+
/** Connect to the sync server at the given URL */
|
|
200
|
+
connect(url: string, options?: TransportOptions): Promise<void>;
|
|
201
|
+
/** Disconnect from the server */
|
|
202
|
+
disconnect(): Promise<void>;
|
|
203
|
+
/** Send a sync message */
|
|
204
|
+
send(message: SyncMessage): void;
|
|
205
|
+
/** Register a handler for incoming messages */
|
|
206
|
+
onMessage(handler: TransportMessageHandler): void;
|
|
207
|
+
/** Register a handler for connection close events */
|
|
208
|
+
onClose(handler: TransportCloseHandler): void;
|
|
209
|
+
/** Register a handler for transport errors */
|
|
210
|
+
onError(handler: TransportErrorHandler): void;
|
|
211
|
+
/** Returns true if the transport is currently connected */
|
|
212
|
+
isConnected(): boolean;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export { type AcknowledgmentMessage as A, type ErrorMessage as E, type HandshakeMessage as H, type OperationBatchMessage as O, type QueueStorage as Q, type SyncMessage as S, type TransportOptions as T, type WireFormat as W, type SerializedOperation as a, type SyncTransport as b, type TransportMessageHandler as c, type TransportCloseHandler as d, type TransportErrorHandler as e, type SyncConfig as f, type SyncStatusInfo as g, type SyncState as h, type HandshakeResponseMessage as i, SYNC_STATES as j, SYNC_STATUSES as k, type SyncScopeContext as l, type SyncStatus as m, isAcknowledgmentMessage as n, isErrorMessage as o, isHandshakeMessage as p, isHandshakeResponseMessage as q, isOperationBatchMessage as r, isSyncMessage as s };
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Operation, OperationType, HLCTimestamp } from '@korajs/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Internal sync engine states. Used for state machine transitions.
|
|
5
|
+
*/
|
|
6
|
+
declare const SYNC_STATES: readonly ["disconnected", "connecting", "handshaking", "syncing", "streaming", "error"];
|
|
7
|
+
type SyncState = (typeof SYNC_STATES)[number];
|
|
8
|
+
/**
|
|
9
|
+
* Developer-facing sync status. Simplified view of the internal state.
|
|
10
|
+
*/
|
|
11
|
+
declare const SYNC_STATUSES: readonly ["connected", "syncing", "synced", "offline", "error"];
|
|
12
|
+
type SyncStatus = (typeof SYNC_STATUSES)[number];
|
|
13
|
+
/**
|
|
14
|
+
* Sync status information exposed to developers.
|
|
15
|
+
*/
|
|
16
|
+
interface SyncStatusInfo {
|
|
17
|
+
/** Current developer-facing status */
|
|
18
|
+
status: SyncStatus;
|
|
19
|
+
/** Number of operations waiting to be sent */
|
|
20
|
+
pendingOperations: number;
|
|
21
|
+
/** Timestamp of last successful sync (null if never synced) */
|
|
22
|
+
lastSyncedAt: number | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Sync configuration provided by the developer.
|
|
26
|
+
*/
|
|
27
|
+
interface SyncConfig {
|
|
28
|
+
/** WebSocket or HTTP URL for the sync server */
|
|
29
|
+
url: string;
|
|
30
|
+
/** Transport type to use. Defaults to 'websocket'. */
|
|
31
|
+
transport?: 'websocket' | 'http';
|
|
32
|
+
/** Auth provider function. Called before each connection attempt. */
|
|
33
|
+
auth?: () => Promise<{
|
|
34
|
+
token: string;
|
|
35
|
+
}>;
|
|
36
|
+
/** Sync scopes per collection. Limits which records sync to this client. */
|
|
37
|
+
scopes?: Record<string, (ctx: SyncScopeContext) => Record<string, unknown>>;
|
|
38
|
+
/** Number of operations per batch. Defaults to 100. */
|
|
39
|
+
batchSize?: number;
|
|
40
|
+
/** Initial reconnection delay in ms. Defaults to 1000. */
|
|
41
|
+
reconnectInterval?: number;
|
|
42
|
+
/** Maximum reconnection delay in ms. Defaults to 30000. */
|
|
43
|
+
maxReconnectInterval?: number;
|
|
44
|
+
/** Schema version of this client. */
|
|
45
|
+
schemaVersion?: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Context passed to sync scope functions.
|
|
49
|
+
*/
|
|
50
|
+
interface SyncScopeContext {
|
|
51
|
+
userId?: string;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Interface for persisting the outbound operation queue.
|
|
56
|
+
* Operations must survive page refreshes and be sent when connection is re-established.
|
|
57
|
+
*/
|
|
58
|
+
interface QueueStorage {
|
|
59
|
+
/** Load all queued operations from persistent storage */
|
|
60
|
+
load(): Promise<Operation[]>;
|
|
61
|
+
/** Persist an operation to the queue */
|
|
62
|
+
enqueue(op: Operation): Promise<void>;
|
|
63
|
+
/** Remove acknowledged operations by their IDs */
|
|
64
|
+
dequeue(ids: string[]): Promise<void>;
|
|
65
|
+
/** Return number of operations in storage */
|
|
66
|
+
count(): Promise<number>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type WireFormat = 'json' | 'protobuf';
|
|
70
|
+
/**
|
|
71
|
+
* Wire-format operation. Plain object (no Map) for JSON serialization.
|
|
72
|
+
* Maps 1:1 with Operation, but uses Record instead of Map for version vectors.
|
|
73
|
+
*/
|
|
74
|
+
interface SerializedOperation {
|
|
75
|
+
id: string;
|
|
76
|
+
nodeId: string;
|
|
77
|
+
type: OperationType;
|
|
78
|
+
collection: string;
|
|
79
|
+
recordId: string;
|
|
80
|
+
data: Record<string, unknown> | null;
|
|
81
|
+
previousData: Record<string, unknown> | null;
|
|
82
|
+
timestamp: HLCTimestamp;
|
|
83
|
+
sequenceNumber: number;
|
|
84
|
+
causalDeps: string[];
|
|
85
|
+
schemaVersion: number;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Handshake message sent by client to initiate sync.
|
|
89
|
+
*/
|
|
90
|
+
interface HandshakeMessage {
|
|
91
|
+
type: 'handshake';
|
|
92
|
+
messageId: string;
|
|
93
|
+
nodeId: string;
|
|
94
|
+
/** Version vector as plain object (nodeId -> sequence number) */
|
|
95
|
+
versionVector: Record<string, number>;
|
|
96
|
+
schemaVersion: number;
|
|
97
|
+
authToken?: string;
|
|
98
|
+
supportedWireFormats?: WireFormat[];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Server response to a handshake.
|
|
102
|
+
*/
|
|
103
|
+
interface HandshakeResponseMessage {
|
|
104
|
+
type: 'handshake-response';
|
|
105
|
+
messageId: string;
|
|
106
|
+
nodeId: string;
|
|
107
|
+
versionVector: Record<string, number>;
|
|
108
|
+
schemaVersion: number;
|
|
109
|
+
accepted: boolean;
|
|
110
|
+
rejectReason?: string;
|
|
111
|
+
selectedWireFormat?: WireFormat;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Batch of operations sent during delta exchange or streaming.
|
|
115
|
+
*/
|
|
116
|
+
interface OperationBatchMessage {
|
|
117
|
+
type: 'operation-batch';
|
|
118
|
+
messageId: string;
|
|
119
|
+
operations: SerializedOperation[];
|
|
120
|
+
/** True if this is the last batch in the delta exchange phase */
|
|
121
|
+
isFinal: boolean;
|
|
122
|
+
/** Index of this batch (0-based) for ordering */
|
|
123
|
+
batchIndex: number;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Acknowledgment of a received message.
|
|
127
|
+
*/
|
|
128
|
+
interface AcknowledgmentMessage {
|
|
129
|
+
type: 'acknowledgment';
|
|
130
|
+
messageId: string;
|
|
131
|
+
acknowledgedMessageId: string;
|
|
132
|
+
lastSequenceNumber: number;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Error message from the server or client.
|
|
136
|
+
*/
|
|
137
|
+
interface ErrorMessage {
|
|
138
|
+
type: 'error';
|
|
139
|
+
messageId: string;
|
|
140
|
+
code: string;
|
|
141
|
+
message: string;
|
|
142
|
+
retriable: boolean;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Union of all sync protocol messages.
|
|
146
|
+
*/
|
|
147
|
+
type SyncMessage = HandshakeMessage | HandshakeResponseMessage | OperationBatchMessage | AcknowledgmentMessage | ErrorMessage;
|
|
148
|
+
/**
|
|
149
|
+
* Check if an unknown value is a valid SyncMessage.
|
|
150
|
+
*/
|
|
151
|
+
declare function isSyncMessage(value: unknown): value is SyncMessage;
|
|
152
|
+
/**
|
|
153
|
+
* Check if a value is a HandshakeMessage.
|
|
154
|
+
*/
|
|
155
|
+
declare function isHandshakeMessage(value: unknown): value is HandshakeMessage;
|
|
156
|
+
/**
|
|
157
|
+
* Check if a value is a HandshakeResponseMessage.
|
|
158
|
+
*/
|
|
159
|
+
declare function isHandshakeResponseMessage(value: unknown): value is HandshakeResponseMessage;
|
|
160
|
+
/**
|
|
161
|
+
* Check if a value is an OperationBatchMessage.
|
|
162
|
+
*/
|
|
163
|
+
declare function isOperationBatchMessage(value: unknown): value is OperationBatchMessage;
|
|
164
|
+
/**
|
|
165
|
+
* Check if a value is an AcknowledgmentMessage.
|
|
166
|
+
*/
|
|
167
|
+
declare function isAcknowledgmentMessage(value: unknown): value is AcknowledgmentMessage;
|
|
168
|
+
/**
|
|
169
|
+
* Check if a value is an ErrorMessage.
|
|
170
|
+
*/
|
|
171
|
+
declare function isErrorMessage(value: unknown): value is ErrorMessage;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Options for transport connection.
|
|
175
|
+
*/
|
|
176
|
+
interface TransportOptions {
|
|
177
|
+
/** Authentication token sent during connection */
|
|
178
|
+
authToken?: string;
|
|
179
|
+
/** Additional headers (for HTTP-based transports) */
|
|
180
|
+
headers?: Record<string, string>;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Transport-level error handler.
|
|
184
|
+
*/
|
|
185
|
+
type TransportErrorHandler = (error: Error) => void;
|
|
186
|
+
/**
|
|
187
|
+
* Transport-level close handler.
|
|
188
|
+
*/
|
|
189
|
+
type TransportCloseHandler = (reason: string) => void;
|
|
190
|
+
/**
|
|
191
|
+
* Transport-level message handler.
|
|
192
|
+
*/
|
|
193
|
+
type TransportMessageHandler = (message: SyncMessage) => void;
|
|
194
|
+
/**
|
|
195
|
+
* Abstract transport interface for sync communication.
|
|
196
|
+
* Implementations can use WebSocket, HTTP, Bluetooth, or any other transport.
|
|
197
|
+
*/
|
|
198
|
+
interface SyncTransport {
|
|
199
|
+
/** Connect to the sync server at the given URL */
|
|
200
|
+
connect(url: string, options?: TransportOptions): Promise<void>;
|
|
201
|
+
/** Disconnect from the server */
|
|
202
|
+
disconnect(): Promise<void>;
|
|
203
|
+
/** Send a sync message */
|
|
204
|
+
send(message: SyncMessage): void;
|
|
205
|
+
/** Register a handler for incoming messages */
|
|
206
|
+
onMessage(handler: TransportMessageHandler): void;
|
|
207
|
+
/** Register a handler for connection close events */
|
|
208
|
+
onClose(handler: TransportCloseHandler): void;
|
|
209
|
+
/** Register a handler for transport errors */
|
|
210
|
+
onError(handler: TransportErrorHandler): void;
|
|
211
|
+
/** Returns true if the transport is currently connected */
|
|
212
|
+
isConnected(): boolean;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export { type AcknowledgmentMessage as A, type ErrorMessage as E, type HandshakeMessage as H, type OperationBatchMessage as O, type QueueStorage as Q, type SyncMessage as S, type TransportOptions as T, type WireFormat as W, type SerializedOperation as a, type SyncTransport as b, type TransportMessageHandler as c, type TransportCloseHandler as d, type TransportErrorHandler as e, type SyncConfig as f, type SyncStatusInfo as g, type SyncState as h, type HandshakeResponseMessage as i, SYNC_STATES as j, SYNC_STATUSES as k, type SyncScopeContext as l, type SyncStatus as m, isAcknowledgmentMessage as n, isErrorMessage as o, isHandshakeMessage as p, isHandshakeResponseMessage as q, isOperationBatchMessage as r, isSyncMessage as s };
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@korajs/sync",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Kora.js sync package",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./internal": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/internal.d.ts",
|
|
23
|
+
"default": "./dist/internal.js"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/internal.d.cts",
|
|
27
|
+
"default": "./dist/internal.cjs"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"protobufjs": "^8.0.1",
|
|
36
|
+
"@korajs/core": "0.1.0",
|
|
37
|
+
"@korajs/merge": "0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@fast-check/vitest": "0.2.0",
|
|
41
|
+
"tsup": "^8.3.6",
|
|
42
|
+
"typescript": "^5.7.3",
|
|
43
|
+
"vitest": "^3.0.4"
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup",
|
|
48
|
+
"dev": "tsup --watch",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"test:chaos": "vitest run tests/integration/chaos-nightly.test.ts",
|
|
51
|
+
"test:watch": "vitest",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"lint": "biome check .",
|
|
54
|
+
"clean": "rm -rf dist .turbo coverage"
|
|
55
|
+
}
|
|
56
|
+
}
|