@loro-dev/flock-sqlite 0.7.0 → 0.9.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/src/lru.ts ADDED
@@ -0,0 +1,74 @@
1
+ export class LruMap<K, V> {
2
+ private readonly map = new Map<K, V>();
3
+
4
+ constructor(private readonly maxSize: number) {
5
+ if (!Number.isFinite(maxSize) || maxSize <= 0) {
6
+ throw new Error(`LruMap maxSize must be a positive number, got ${maxSize}`);
7
+ }
8
+ }
9
+
10
+ get size(): number {
11
+ return this.map.size;
12
+ }
13
+
14
+ has(key: K): boolean {
15
+ return this.map.has(key);
16
+ }
17
+
18
+ get(key: K): V | undefined {
19
+ if (!this.map.has(key)) {
20
+ return undefined;
21
+ }
22
+ const value = this.map.get(key) as V;
23
+ // Refresh recency.
24
+ this.map.delete(key);
25
+ this.map.set(key, value);
26
+ return value;
27
+ }
28
+
29
+ set(key: K, value: V): void {
30
+ if (this.map.has(key)) {
31
+ this.map.delete(key);
32
+ }
33
+ this.map.set(key, value);
34
+ this.evictIfNeeded();
35
+ }
36
+
37
+ delete(key: K): boolean {
38
+ return this.map.delete(key);
39
+ }
40
+
41
+ clear(): void {
42
+ this.map.clear();
43
+ }
44
+
45
+ private evictIfNeeded(): void {
46
+ while (this.map.size > this.maxSize) {
47
+ const oldest = this.map.keys().next().value as K | undefined;
48
+ if (oldest === undefined) {
49
+ return;
50
+ }
51
+ this.map.delete(oldest);
52
+ }
53
+ }
54
+ }
55
+
56
+ export class LruSet<T> {
57
+ private readonly map: LruMap<T, true>;
58
+
59
+ constructor(maxSize: number) {
60
+ this.map = new LruMap<T, true>(maxSize);
61
+ }
62
+
63
+ has(value: T): boolean {
64
+ return this.map.get(value) !== undefined;
65
+ }
66
+
67
+ add(value: T): void {
68
+ this.map.set(value, true);
69
+ }
70
+
71
+ clear(): void {
72
+ this.map.clear();
73
+ }
74
+ }
@@ -0,0 +1,79 @@
1
+ import type { FlockSQLiteRole } from "./types";
2
+
3
+ export type FlockTransport = {
4
+ postMessage(message: unknown): void;
5
+ subscribe(onMessage: (message: unknown) => void): () => void;
6
+ close?: () => void;
7
+ };
8
+
9
+ export type FlockTransportFactory = (name: string) => FlockTransport | undefined;
10
+
11
+ export type FlockRoleProvider = {
12
+ getRole(): FlockSQLiteRole;
13
+ subscribeRoleChange?: (listener: (role: FlockSQLiteRole) => void) => () => void;
14
+ };
15
+
16
+ export type TimeoutHandle = unknown;
17
+
18
+ export type FlockRuntime = {
19
+ now(): number;
20
+ setTimeout(fn: () => void, ms: number): TimeoutHandle;
21
+ clearTimeout(handle: TimeoutHandle): void;
22
+ randomUUID(): string;
23
+ };
24
+
25
+ export function createDefaultRuntime(): FlockRuntime {
26
+ return {
27
+ now: () => Date.now(),
28
+ setTimeout: (fn, ms) => setTimeout(fn, ms) as unknown,
29
+ clearTimeout: (handle) => clearTimeout(handle as any),
30
+ randomUUID: () => {
31
+ try {
32
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
33
+ return crypto.randomUUID();
34
+ }
35
+ } catch {
36
+ // ignore
37
+ }
38
+ return `${Date.now()}_${Math.random().toString(16).slice(2)}`;
39
+ },
40
+ };
41
+ }
42
+
43
+ export function createBroadcastChannelTransport(name: string): FlockTransport | undefined {
44
+ if (typeof BroadcastChannel === "undefined") {
45
+ return undefined;
46
+ }
47
+
48
+ try {
49
+ const channel = new BroadcastChannel(name);
50
+
51
+ return {
52
+ postMessage: (message) => {
53
+ try {
54
+ channel.postMessage(message);
55
+ } catch {
56
+ // Swallow transport errors to avoid breaking the CRDT/sync pipeline.
57
+ }
58
+ },
59
+ subscribe: (onMessage) => {
60
+ const handler = (event: MessageEvent) => {
61
+ onMessage(event.data);
62
+ };
63
+ channel.addEventListener("message", handler);
64
+ return () => {
65
+ channel.removeEventListener("message", handler);
66
+ };
67
+ },
68
+ close: () => {
69
+ try {
70
+ channel.close();
71
+ } catch {
72
+ // ignore
73
+ }
74
+ },
75
+ };
76
+ } catch {
77
+ return undefined;
78
+ }
79
+ }
@@ -0,0 +1,107 @@
1
+ import type { EntryClock, ExportBundle, ExportPayload, KeyPart, Value } from "./types";
2
+
3
+ export type TabId = string;
4
+ export type RequestId = string;
5
+
6
+ export type FlockCommit = {
7
+ commitId: string;
8
+ origin: TabId;
9
+ source: string;
10
+ events: Array<{
11
+ key: KeyPart[];
12
+ clock: EntryClock;
13
+ payload: ExportPayload;
14
+ }>;
15
+ /** Optional meta updates that consumers should apply. */
16
+ meta?: {
17
+ peerId?: string;
18
+ };
19
+ };
20
+
21
+ export type FlockWriteRequest =
22
+ | {
23
+ kind: "apply";
24
+ source: "local";
25
+ key: KeyPart[];
26
+ payload: ExportPayload;
27
+ now?: number;
28
+ skipSameValue: boolean;
29
+ }
30
+ | {
31
+ kind: "putMvr";
32
+ source: "local";
33
+ key: KeyPart[];
34
+ value: Value;
35
+ now?: number;
36
+ }
37
+ | {
38
+ kind: "import";
39
+ source: "import";
40
+ bundle: ExportBundle;
41
+ }
42
+ | {
43
+ kind: "setPeerId";
44
+ source: "meta";
45
+ peerId: string;
46
+ };
47
+
48
+ export type FlockRpcRequest = {
49
+ t: "req";
50
+ from: TabId;
51
+ id: RequestId;
52
+ payload: FlockWriteRequest;
53
+ };
54
+
55
+ export type FlockRpcResponse = {
56
+ t: "res";
57
+ to: TabId;
58
+ id: RequestId;
59
+ ok: boolean;
60
+ commit?: FlockCommit;
61
+ result?: unknown;
62
+ error?: { name: string; message: string; stack?: string };
63
+ };
64
+
65
+ export type FlockCommitMessage = {
66
+ t: "commit";
67
+ commit: FlockCommit;
68
+ };
69
+
70
+ export type FlockChannelMessage = FlockRpcRequest | FlockRpcResponse | FlockCommitMessage;
71
+
72
+ export function isFlockChannelMessage(message: unknown): message is FlockChannelMessage {
73
+ if (!message || typeof message !== "object") {
74
+ return false;
75
+ }
76
+ const candidate = message as { t?: unknown };
77
+ return candidate.t === "req" || candidate.t === "res" || candidate.t === "commit";
78
+ }
79
+
80
+ export function createRequestId(): RequestId {
81
+ try {
82
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
83
+ return crypto.randomUUID();
84
+ }
85
+ } catch {
86
+ // ignore
87
+ }
88
+ return `req_${Date.now()}_${Math.random().toString(16).slice(2)}`;
89
+ }
90
+
91
+ export function createTabId(): TabId {
92
+ try {
93
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
94
+ return crypto.randomUUID();
95
+ }
96
+ } catch {
97
+ // ignore
98
+ }
99
+ return `tab_${Date.now()}_${Math.random().toString(16).slice(2)}`;
100
+ }
101
+
102
+ export function serializeError(error: unknown): { name: string; message: string; stack?: string } {
103
+ if (error instanceof Error) {
104
+ return { name: error.name, message: error.message, stack: error.stack };
105
+ }
106
+ return { name: "Error", message: String(error) };
107
+ }
package/src/types.ts CHANGED
@@ -101,6 +101,7 @@ export type EventPayload = ExportPayload;
101
101
 
102
102
  export type Event = {
103
103
  key: KeyPart[];
104
+ clock: EntryClock;
104
105
  value?: Value;
105
106
  metadata?: MetadataMap;
106
107
  payload: EventPayload;
@@ -111,6 +112,8 @@ export type EventBatch = {
111
112
  events: Event[];
112
113
  };
113
114
 
115
+ export type FlockSQLiteRole = "host" | "participant" | "unknown";
116
+
114
117
  export type ExportOptions = {
115
118
  from?: VersionVector;
116
119
  hooks?: ExportHooks;
@@ -144,3 +147,5 @@ export type PutWithMetaOptions = {
144
147
  };
145
148
 
146
149
  export type EventListener = (batch: EventBatch) => void;
150
+
151
+ export type RoleChangeListener = (role: FlockSQLiteRole) => void;