@byearlybird/starling 0.11.1 → 0.13.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.
@@ -1,191 +0,0 @@
1
- //#region src/plugins/http/index.ts
2
- /**
3
- * Create an HTTP sync plugin for Starling databases.
4
- *
5
- * The plugin:
6
- * - Fetches all collections from the server on init (single attempt)
7
- * - Polls the server at regular intervals to fetch updates (with retry)
8
- * - Debounces local mutations and pushes them to the server (with retry)
9
- * - Supports request/response hooks for authentication, encryption, etc.
10
- *
11
- * @param config - HTTP plugin configuration
12
- * @returns A DatabasePlugin instance
13
- *
14
- * @example
15
- * ```typescript
16
- * const db = await createDatabase({
17
- * name: "my-app",
18
- * schema: {
19
- * tasks: { schema: taskSchema, getId: (task) => task.id },
20
- * },
21
- * })
22
- * .use(httpPlugin({
23
- * baseUrl: "https://api.example.com",
24
- * onRequest: () => ({
25
- * headers: { Authorization: `Bearer ${token}` }
26
- * })
27
- * }))
28
- * .init();
29
- * ```
30
- *
31
- * @example With encryption
32
- * ```typescript
33
- * const db = await createDatabase({
34
- * name: "my-app",
35
- * schema: {
36
- * tasks: { schema: taskSchema, getId: (task) => task.id },
37
- * },
38
- * })
39
- * .use(httpPlugin({
40
- * baseUrl: "https://api.example.com",
41
- * onRequest: ({ document }) => ({
42
- * headers: { Authorization: `Bearer ${token}` },
43
- * document: document ? encrypt(document) : undefined
44
- * }),
45
- * onResponse: ({ document }) => ({
46
- * document: decrypt(document)
47
- * })
48
- * }))
49
- * .init();
50
- * ```
51
- */
52
- function httpPlugin(config) {
53
- const { baseUrl, pollingInterval = 5e3, debounceDelay = 1e3, onRequest, onResponse, retry = {} } = config;
54
- const { maxAttempts = 3, initialDelay = 1e3, maxDelay = 3e4 } = retry;
55
- let pollingTimer = null;
56
- let unsubscribe = null;
57
- const debounceTimers = /* @__PURE__ */ new Map();
58
- return { handlers: {
59
- async init(db) {
60
- const collectionNames = db.collectionKeys();
61
- for (const collectionName of collectionNames) try {
62
- await fetchCollection(db, collectionName, baseUrl, onRequest, onResponse, false);
63
- } catch (error) {
64
- console.error(`Failed to fetch collection "${String(collectionName)}" during init:`, error);
65
- }
66
- pollingTimer = setInterval(async () => {
67
- for (const collectionName of collectionNames) try {
68
- await fetchCollection(db, collectionName, baseUrl, onRequest, onResponse, true, maxAttempts, initialDelay, maxDelay);
69
- } catch (error) {
70
- console.error(`Failed to poll collection "${String(collectionName)}":`, error);
71
- }
72
- }, pollingInterval);
73
- unsubscribe = db.on("mutation", (event) => {
74
- const collectionName = event.collection;
75
- const key = String(collectionName);
76
- const existingTimer = debounceTimers.get(key);
77
- if (existingTimer) clearTimeout(existingTimer);
78
- const timer = setTimeout(async () => {
79
- debounceTimers.delete(key);
80
- try {
81
- await pushCollection(db, collectionName, baseUrl, onRequest, onResponse, maxAttempts, initialDelay, maxDelay);
82
- } catch (error) {
83
- console.error(`Failed to push collection "${String(collectionName)}":`, error);
84
- }
85
- }, debounceDelay);
86
- debounceTimers.set(key, timer);
87
- });
88
- },
89
- async dispose(_db) {
90
- if (pollingTimer) {
91
- clearInterval(pollingTimer);
92
- pollingTimer = null;
93
- }
94
- for (const timer of debounceTimers.values()) clearTimeout(timer);
95
- debounceTimers.clear();
96
- if (unsubscribe) {
97
- unsubscribe();
98
- unsubscribe = null;
99
- }
100
- }
101
- } };
102
- }
103
- /**
104
- * Fetch a collection from the server (GET request)
105
- */
106
- async function fetchCollection(db, collectionName, baseUrl, onRequest, onResponse, enableRetry, maxAttempts = 3, initialDelay = 1e3, maxDelay = 3e4) {
107
- const url = `${baseUrl}/${db.name}/${String(collectionName)}`;
108
- const requestResult = onRequest?.({
109
- collection: String(collectionName),
110
- operation: "GET",
111
- url
112
- });
113
- if (requestResult && "skip" in requestResult && requestResult.skip) return;
114
- const headers = requestResult && "headers" in requestResult ? requestResult.headers : void 0;
115
- const executeRequest = async () => {
116
- const response = await fetch(url, {
117
- method: "GET",
118
- headers: {
119
- "Content-Type": "application/json",
120
- ...headers
121
- }
122
- });
123
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
124
- const document = await response.json();
125
- const responseResult = onResponse?.({
126
- collection: String(collectionName),
127
- document
128
- });
129
- if (responseResult && "skip" in responseResult && responseResult.skip) return;
130
- const finalDocument = responseResult && "document" in responseResult ? responseResult.document : document;
131
- db[collectionName].merge(finalDocument);
132
- };
133
- if (enableRetry) await withRetry(executeRequest, maxAttempts, initialDelay, maxDelay);
134
- else await executeRequest();
135
- }
136
- /**
137
- * Push a collection to the server (PATCH request)
138
- */
139
- async function pushCollection(db, collectionName, baseUrl, onRequest, onResponse, maxAttempts = 3, initialDelay = 1e3, maxDelay = 3e4) {
140
- const url = `${baseUrl}/${db.name}/${String(collectionName)}`;
141
- const document = db[collectionName].toDocument();
142
- const requestResult = onRequest?.({
143
- collection: String(collectionName),
144
- operation: "PATCH",
145
- url,
146
- document
147
- });
148
- if (requestResult && "skip" in requestResult && requestResult.skip) return;
149
- const headers = requestResult && "headers" in requestResult ? requestResult.headers : void 0;
150
- const requestDocument = requestResult && "document" in requestResult ? requestResult.document : document;
151
- const executeRequest = async () => {
152
- const response = await fetch(url, {
153
- method: "PATCH",
154
- headers: {
155
- "Content-Type": "application/json",
156
- ...headers
157
- },
158
- body: JSON.stringify(requestDocument)
159
- });
160
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
161
- const responseDocument = await response.json();
162
- const responseResult = onResponse?.({
163
- collection: String(collectionName),
164
- document: responseDocument
165
- });
166
- if (responseResult && "skip" in responseResult && responseResult.skip) return;
167
- const finalDocument = responseResult && "document" in responseResult ? responseResult.document : responseDocument;
168
- db[collectionName].merge(finalDocument);
169
- };
170
- await withRetry(executeRequest, maxAttempts, initialDelay, maxDelay);
171
- }
172
- /**
173
- * Execute a function with exponential backoff retry logic
174
- */
175
- async function withRetry(fn, maxAttempts, initialDelay, maxDelay) {
176
- let lastError;
177
- let delay = initialDelay;
178
- for (let attempt = 0; attempt < maxAttempts; attempt++) try {
179
- return await fn();
180
- } catch (error) {
181
- lastError = error instanceof Error ? error : new Error(String(error));
182
- if (attempt < maxAttempts - 1) {
183
- await new Promise((resolve) => setTimeout(resolve, delay));
184
- delay = Math.min(delay * 2, maxDelay);
185
- }
186
- }
187
- throw lastError;
188
- }
189
-
190
- //#endregion
191
- export { httpPlugin };
@@ -1,59 +0,0 @@
1
- import "./index-D7bXWDg6.js";
2
- import { r as DatabasePlugin } from "./db-DJ_6dO-K.js";
3
-
4
- //#region src/plugins/idb/index.d.ts
5
- type IdbPluginConfig = {
6
- /**
7
- * Version of the IndexedDB database
8
- * @default 1
9
- */
10
- version?: number;
11
- /**
12
- * Use BroadcastChannel API for instant cross-tab sync
13
- * @default true
14
- */
15
- useBroadcastChannel?: boolean;
16
- };
17
- /**
18
- * Create an IndexedDB persistence plugin for Starling databases.
19
- *
20
- * The plugin:
21
- * - Loads existing documents from IndexedDB on init
22
- * - Persists all documents to IndexedDB on every mutation
23
- * - Enables instant cross-tab sync via BroadcastChannel API
24
- * - Gracefully closes the database connection on dispose
25
- *
26
- * Cross-tab sync uses the BroadcastChannel API to notify other tabs
27
- * of changes in real-time. When a mutation occurs in one tab, other tabs
28
- * are instantly notified and reload the data from IndexedDB.
29
- *
30
- * @param config - IndexedDB configuration
31
- * @returns A DatabasePlugin instance
32
- *
33
- * @example
34
- * ```typescript
35
- * const db = await createDatabase({
36
- * name: "my-app",
37
- * schema: {
38
- * tasks: { schema: taskSchema, getId: (task) => task.id },
39
- * },
40
- * })
41
- * .use(idbPlugin())
42
- * .init();
43
- * ```
44
- *
45
- * @example Disable BroadcastChannel
46
- * ```typescript
47
- * const db = await createDatabase({
48
- * name: "my-app",
49
- * schema: {
50
- * tasks: { schema: taskSchema, getId: (task) => task.id },
51
- * },
52
- * })
53
- * .use(idbPlugin({ useBroadcastChannel: false }))
54
- * .init();
55
- * ```
56
- */
57
- declare function idbPlugin(config?: IdbPluginConfig): DatabasePlugin<any>;
58
- //#endregion
59
- export { IdbPluginConfig, idbPlugin };
@@ -1,169 +0,0 @@
1
- //#region src/plugins/idb/index.ts
2
- /**
3
- * Create an IndexedDB persistence plugin for Starling databases.
4
- *
5
- * The plugin:
6
- * - Loads existing documents from IndexedDB on init
7
- * - Persists all documents to IndexedDB on every mutation
8
- * - Enables instant cross-tab sync via BroadcastChannel API
9
- * - Gracefully closes the database connection on dispose
10
- *
11
- * Cross-tab sync uses the BroadcastChannel API to notify other tabs
12
- * of changes in real-time. When a mutation occurs in one tab, other tabs
13
- * are instantly notified and reload the data from IndexedDB.
14
- *
15
- * @param config - IndexedDB configuration
16
- * @returns A DatabasePlugin instance
17
- *
18
- * @example
19
- * ```typescript
20
- * const db = await createDatabase({
21
- * name: "my-app",
22
- * schema: {
23
- * tasks: { schema: taskSchema, getId: (task) => task.id },
24
- * },
25
- * })
26
- * .use(idbPlugin())
27
- * .init();
28
- * ```
29
- *
30
- * @example Disable BroadcastChannel
31
- * ```typescript
32
- * const db = await createDatabase({
33
- * name: "my-app",
34
- * schema: {
35
- * tasks: { schema: taskSchema, getId: (task) => task.id },
36
- * },
37
- * })
38
- * .use(idbPlugin({ useBroadcastChannel: false }))
39
- * .init();
40
- * ```
41
- */
42
- function idbPlugin(config = {}) {
43
- const { version = 1, useBroadcastChannel = true } = config;
44
- let dbInstance = null;
45
- let unsubscribe = null;
46
- let broadcastChannel = null;
47
- const instanceId = crypto.randomUUID();
48
- return { handlers: {
49
- async init(db) {
50
- const collectionNames = db.collectionKeys();
51
- dbInstance = await openDatabase(db.name, version, collectionNames);
52
- const savedDocs = await loadDocuments(dbInstance, collectionNames);
53
- for (const collectionName of Object.keys(savedDocs)) {
54
- const doc = savedDocs[collectionName];
55
- const collection = db[collectionName];
56
- if (doc && collection) collection.merge(doc);
57
- }
58
- unsubscribe = db.on("mutation", async () => {
59
- if (dbInstance) {
60
- const docs = db.toDocuments();
61
- await saveDocuments(dbInstance, docs);
62
- if (broadcastChannel) broadcastChannel.postMessage({
63
- type: "mutation",
64
- instanceId,
65
- timestamp: Date.now()
66
- });
67
- }
68
- });
69
- if (useBroadcastChannel && typeof BroadcastChannel !== "undefined") {
70
- broadcastChannel = new BroadcastChannel(`starling:${db.name}`);
71
- broadcastChannel.onmessage = async (event) => {
72
- if (event.data.instanceId === instanceId) return;
73
- if (event.data.type === "mutation" && dbInstance) {
74
- const savedDocs$1 = await loadDocuments(dbInstance, collectionNames);
75
- for (const collectionName of Object.keys(savedDocs$1)) {
76
- const doc = savedDocs$1[collectionName];
77
- const collection = db[collectionName];
78
- if (doc && collection) collection.merge(doc);
79
- }
80
- }
81
- };
82
- }
83
- },
84
- async dispose(db) {
85
- if (broadcastChannel) {
86
- broadcastChannel.close();
87
- broadcastChannel = null;
88
- }
89
- if (unsubscribe) {
90
- unsubscribe();
91
- unsubscribe = null;
92
- }
93
- if (dbInstance) {
94
- const docs = db.toDocuments();
95
- await saveDocuments(dbInstance, docs);
96
- dbInstance.close();
97
- dbInstance = null;
98
- }
99
- }
100
- } };
101
- }
102
- /**
103
- * Open an IndexedDB database and create object stores for each collection
104
- */
105
- function openDatabase(dbName, version, collectionNames) {
106
- return new Promise((resolve, reject) => {
107
- const request = indexedDB.open(dbName, version);
108
- request.onerror = () => {
109
- reject(/* @__PURE__ */ new Error(`Failed to open IndexedDB: ${request.error?.message}`));
110
- };
111
- request.onsuccess = () => {
112
- resolve(request.result);
113
- };
114
- request.onupgradeneeded = (event) => {
115
- const db = event.target.result;
116
- for (const collectionName of collectionNames) if (!db.objectStoreNames.contains(collectionName)) db.createObjectStore(collectionName);
117
- };
118
- });
119
- }
120
- /**
121
- * Load documents from IndexedDB for all collections
122
- */
123
- async function loadDocuments(db, collectionNames) {
124
- const documents = {};
125
- for (const collectionName of collectionNames) if (db.objectStoreNames.contains(collectionName)) {
126
- const doc = await getFromStore(db, collectionName, "document");
127
- if (doc) documents[collectionName] = doc;
128
- }
129
- return documents;
130
- }
131
- /**
132
- * Save documents to IndexedDB for all collections
133
- */
134
- async function saveDocuments(db, documents) {
135
- const promises = [];
136
- for (const collectionName of Object.keys(documents)) if (db.objectStoreNames.contains(collectionName)) promises.push(putToStore(db, collectionName, "document", documents[collectionName]));
137
- await Promise.all(promises);
138
- }
139
- /**
140
- * Get a value from an IndexedDB object store
141
- */
142
- function getFromStore(db, storeName, key) {
143
- return new Promise((resolve, reject) => {
144
- const request = db.transaction(storeName, "readonly").objectStore(storeName).get(key);
145
- request.onerror = () => {
146
- reject(/* @__PURE__ */ new Error(`Failed to get from store ${storeName}: ${request.error?.message}`));
147
- };
148
- request.onsuccess = () => {
149
- resolve(request.result ?? null);
150
- };
151
- });
152
- }
153
- /**
154
- * Put a value into an IndexedDB object store
155
- */
156
- function putToStore(db, storeName, key, value) {
157
- return new Promise((resolve, reject) => {
158
- const request = db.transaction(storeName, "readwrite").objectStore(storeName).put(value, key);
159
- request.onerror = () => {
160
- reject(/* @__PURE__ */ new Error(`Failed to put to store ${storeName}: ${request.error?.message}`));
161
- };
162
- request.onsuccess = () => {
163
- resolve();
164
- };
165
- });
166
- }
167
-
168
- //#endregion
169
- export { idbPlugin };