@basictech/react 0.8.0-beta.4 → 0.9.0-beta.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/changelog.md +31 -0
- package/dist/index.d.mts +1025 -294
- package/dist/index.d.ts +1025 -294
- package/dist/index.js +2256 -1444
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2230 -1448
- package/dist/index.mjs.map +1 -1
- package/package.json +27 -8
- package/readme.md +280 -217
- package/.turbo/turbo-build.log +0 -24
- package/AUTH_IMPLEMENTATION_GUIDE.md +0 -2011
- package/src/AuthContext.tsx +0 -591
- package/src/config.ts +0 -9
- package/src/context.tsx +0 -122
- package/src/core/auth/AuthManager.ts +0 -1371
- package/src/core/db/RemoteCollection.ts +0 -308
- package/src/core/db/RemoteDB.ts +0 -40
- package/src/core/db/index.ts +0 -7
- package/src/core/db/types.ts +0 -140
- package/src/dev/BasicDevToolbar.tsx +0 -665
- package/src/index.ts +0 -36
- package/src/sync/index.ts +0 -288
- package/src/sync/syncProtocol.js +0 -291
- package/src/sync/tokenRegistry.ts +0 -20
- package/src/updater/updateMigrations.ts +0 -22
- package/src/updater/versionUpdater.ts +0 -153
- package/src/utils/network.ts +0 -135
- package/src/utils/normalizeClientId.ts +0 -22
- package/src/utils/resolveDid.ts +0 -101
- package/src/utils/schema.ts +0 -119
- package/src/utils/storage.ts +0 -67
- package/tsconfig.json +0 -9
- package/tsup.config.ts +0 -11
package/src/sync/index.ts
DELETED
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { v7 as uuidv7 } from 'uuid';
|
|
4
|
-
import { Dexie } from 'dexie';
|
|
5
|
-
|
|
6
|
-
import { log } from '../config'
|
|
7
|
-
import { validateData } from '@basictech/schema'
|
|
8
|
-
import { setTokenGetter } from './tokenRegistry'
|
|
9
|
-
|
|
10
|
-
// Track initialization state
|
|
11
|
-
let dexieExtensionsLoaded = false;
|
|
12
|
-
let initPromise: Promise<void> | null = null;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Initialize Dexie extensions (syncable and observable)
|
|
16
|
-
* This must be called before creating a BasicSync instance
|
|
17
|
-
* Safe to call multiple times - will only load once
|
|
18
|
-
*/
|
|
19
|
-
export async function initDexieExtensions(): Promise<void> {
|
|
20
|
-
// Return early if already loaded or not in browser
|
|
21
|
-
if (dexieExtensionsLoaded) return;
|
|
22
|
-
if (typeof window === 'undefined') return;
|
|
23
|
-
|
|
24
|
-
// If already initializing, wait for that promise
|
|
25
|
-
if (initPromise) return initPromise;
|
|
26
|
-
|
|
27
|
-
initPromise = (async () => {
|
|
28
|
-
try {
|
|
29
|
-
// Dynamic imports - only loaded in browser
|
|
30
|
-
await import('dexie-syncable');
|
|
31
|
-
await import('dexie-observable');
|
|
32
|
-
|
|
33
|
-
// Import and register sync protocol
|
|
34
|
-
const { syncProtocol } = await import('./syncProtocol');
|
|
35
|
-
syncProtocol();
|
|
36
|
-
|
|
37
|
-
dexieExtensionsLoaded = true;
|
|
38
|
-
log('Dexie extensions loaded successfully');
|
|
39
|
-
} catch (error) {
|
|
40
|
-
console.error('Failed to load Dexie extensions:', error);
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
})();
|
|
44
|
-
|
|
45
|
-
return initPromise;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Check if Dexie extensions are loaded
|
|
50
|
-
*/
|
|
51
|
-
export function isDexieReady(): boolean {
|
|
52
|
-
return dexieExtensionsLoaded;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
export class BasicSync extends Dexie {
|
|
57
|
-
basic_schema: any
|
|
58
|
-
|
|
59
|
-
constructor(name: string, options: any) {
|
|
60
|
-
super(name, options);
|
|
61
|
-
|
|
62
|
-
// --- INIT SCHEMA --- //
|
|
63
|
-
this.basic_schema = options.schema
|
|
64
|
-
this.version(1).stores(this._convertSchemaToDxSchema(this.basic_schema))
|
|
65
|
-
this.version(2).stores({})
|
|
66
|
-
|
|
67
|
-
// @ts-ignore - alias for toArray
|
|
68
|
-
this.Collection.prototype.get = this.Collection.prototype.toArray
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async connect({ getToken, ws_url }: { getToken: (opts?: { forceRefresh?: boolean }) => Promise<string>, ws_url?: string }) {
|
|
72
|
-
const WS_URL = ws_url || 'wss://pds.basic.id/ws'
|
|
73
|
-
|
|
74
|
-
log('Connecting to', WS_URL)
|
|
75
|
-
|
|
76
|
-
// Store getToken in module-level registry (not in options) because
|
|
77
|
-
// dexie-syncable serializes options into IndexedDB via structured clone,
|
|
78
|
-
// which cannot handle functions.
|
|
79
|
-
setTokenGetter(WS_URL, getToken)
|
|
80
|
-
|
|
81
|
-
await this.updateSyncNodes();
|
|
82
|
-
|
|
83
|
-
log('Starting connection...')
|
|
84
|
-
return this.syncable.connect("websocket", WS_URL, { schema: this.basic_schema });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async disconnect({ ws_url }: { ws_url?: string } = {}) {
|
|
88
|
-
const WS_URL = ws_url || 'wss://pds.basic.id/ws'
|
|
89
|
-
|
|
90
|
-
return this.syncable.disconnect(WS_URL)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
private async updateSyncNodes() {
|
|
94
|
-
try {
|
|
95
|
-
const syncNodes = await this.table('_syncNodes').toArray();
|
|
96
|
-
const localSyncNodes = syncNodes.filter(node => node.type === 'local');
|
|
97
|
-
log('Local sync nodes:', localSyncNodes);
|
|
98
|
-
|
|
99
|
-
if (localSyncNodes.length > 1) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const largestNodeId = Math.max(...localSyncNodes.map(node => node.id));
|
|
103
|
-
// Check if the largest node is already the master
|
|
104
|
-
const largestNode = localSyncNodes.find(node => node.id === largestNodeId);
|
|
105
|
-
if (largestNode && largestNode.isMaster === 1) {
|
|
106
|
-
log('Largest node is already the master. No changes needed.');
|
|
107
|
-
return; // Exit the function early as no changes are needed
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
log('Largest node id:', largestNodeId);
|
|
112
|
-
log('HEISENBUG: More than one local sync node found.')
|
|
113
|
-
|
|
114
|
-
for (const node of localSyncNodes) {
|
|
115
|
-
log(`Local sync node keys:`, node.id, node.isMaster);
|
|
116
|
-
await this.table('_syncNodes').update(node.id, { isMaster: node.id === largestNodeId ? 1 : 0 });
|
|
117
|
-
|
|
118
|
-
log(`HEISENBUG: Setting ${node.id} to ${node.id === largestNodeId ? 'master' : '0'}`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
122
|
-
|
|
123
|
-
if (typeof window !== 'undefined') {
|
|
124
|
-
window.location.reload();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
log('Sync nodes updated');
|
|
129
|
-
} catch (error) {
|
|
130
|
-
console.error('Error updating _syncNodes table:', error);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
handleStatusChange(fn: any) {
|
|
135
|
-
this.syncable.on("statusChanged", fn)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
_convertSchemaToDxSchema(schema: any) {
|
|
140
|
-
const stores = Object.entries(schema.tables).map(([key, table]: any) => {
|
|
141
|
-
const indexedFields = Object.entries(table.fields)
|
|
142
|
-
.filter(([, field]: any) => field.indexed)
|
|
143
|
-
.map(([fieldKey]: any) => `,${fieldKey}`)
|
|
144
|
-
.join('')
|
|
145
|
-
return {
|
|
146
|
-
[key]: 'id' + indexedFields
|
|
147
|
-
}
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
return Object.assign({}, ...stores)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
debugeroo() {
|
|
154
|
-
return this.syncable
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
collection<T extends { id: string } = Record<string, any> & { id: string }>(name: string) {
|
|
158
|
-
// Validate table exists in schema
|
|
159
|
-
if (this.basic_schema?.tables && !this.basic_schema.tables[name]) {
|
|
160
|
-
throw new Error(`Table "${name}" not found in schema`)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const table = this.table(name)
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
/**
|
|
167
|
-
* Returns the underlying Dexie table
|
|
168
|
-
* @type {Dexie.Table}
|
|
169
|
-
*/
|
|
170
|
-
ref: table,
|
|
171
|
-
|
|
172
|
-
// --- WRITE ---- //
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Add a new record - returns the full object with generated id
|
|
176
|
-
*/
|
|
177
|
-
add: async (data: Omit<T, 'id'>): Promise<T> => {
|
|
178
|
-
const valid = validateData(this.basic_schema, name, data)
|
|
179
|
-
if (!valid.valid) {
|
|
180
|
-
log('Invalid data', valid)
|
|
181
|
-
throw new Error(valid.message || 'Data validation failed')
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const id = uuidv7()
|
|
185
|
-
const fullData = { id, ...data } as T
|
|
186
|
-
|
|
187
|
-
await table.add(fullData)
|
|
188
|
-
return fullData
|
|
189
|
-
},
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Put (upsert) a record - returns the full object
|
|
193
|
-
*/
|
|
194
|
-
put: async (data: T): Promise<T> => {
|
|
195
|
-
if (!data.id) {
|
|
196
|
-
throw new Error('put() requires an id field')
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const valid = validateData(this.basic_schema, name, data)
|
|
200
|
-
if (!valid.valid) {
|
|
201
|
-
log('Invalid data', valid)
|
|
202
|
-
throw new Error(valid.message || 'Data validation failed')
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
await table.put(data)
|
|
206
|
-
return data
|
|
207
|
-
},
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Update an existing record - returns updated object or null
|
|
211
|
-
*/
|
|
212
|
-
update: async (id: string, data: Partial<Omit<T, 'id'>>): Promise<T | null> => {
|
|
213
|
-
if (!id) {
|
|
214
|
-
throw new Error('update() requires an id')
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const valid = validateData(this.basic_schema, name, data, false)
|
|
218
|
-
if (!valid.valid) {
|
|
219
|
-
log('Invalid data', valid)
|
|
220
|
-
throw new Error(valid.message || 'Data validation failed')
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const updated = await table.update(id, data)
|
|
224
|
-
if (updated === 0) {
|
|
225
|
-
return null
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Fetch and return the updated record
|
|
229
|
-
const record = await table.get(id)
|
|
230
|
-
return (record as T) || null
|
|
231
|
-
},
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Delete a record - returns true if deleted, false if not found
|
|
235
|
-
*/
|
|
236
|
-
delete: async (id: string): Promise<boolean> => {
|
|
237
|
-
if (!id) {
|
|
238
|
-
throw new Error('delete() requires an id')
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Check if record exists first
|
|
242
|
-
const exists = await table.get(id)
|
|
243
|
-
if (!exists) {
|
|
244
|
-
return false
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
await table.delete(id)
|
|
248
|
-
return true
|
|
249
|
-
},
|
|
250
|
-
|
|
251
|
-
// --- READ ---- //
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Get a single record by id - returns null if not found
|
|
255
|
-
*/
|
|
256
|
-
get: async (id: string): Promise<T | null> => {
|
|
257
|
-
if (!id) {
|
|
258
|
-
throw new Error('get() requires an id')
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const record = await table.get(id)
|
|
262
|
-
return (record as T) || null
|
|
263
|
-
},
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Get all records in the collection
|
|
267
|
-
*/
|
|
268
|
-
getAll: async (): Promise<T[]> => {
|
|
269
|
-
return table.toArray() as Promise<T[]>
|
|
270
|
-
},
|
|
271
|
-
|
|
272
|
-
// --- QUERY ---- //
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Filter records using a predicate function
|
|
276
|
-
*/
|
|
277
|
-
filter: async (fn: (item: T) => boolean): Promise<T[]> => {
|
|
278
|
-
return table.filter(fn).toArray() as Promise<T[]>
|
|
279
|
-
},
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Get the raw Dexie table for advanced queries
|
|
283
|
-
* @deprecated Use ref instead
|
|
284
|
-
*/
|
|
285
|
-
query: () => table,
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
package/src/sync/syncProtocol.js
DELETED
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
import { Dexie } from "dexie";
|
|
3
|
-
import { log } from "../config";
|
|
4
|
-
import { getTokenGetter } from "./tokenRegistry";
|
|
5
|
-
|
|
6
|
-
function decodeJwtExp(token) {
|
|
7
|
-
try {
|
|
8
|
-
var parts = token.split(".");
|
|
9
|
-
if (parts.length !== 3) return null;
|
|
10
|
-
var payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
11
|
-
return typeof payload.exp === "number" ? payload.exp : null;
|
|
12
|
-
} catch (_) {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const syncProtocol = function () {
|
|
18
|
-
log("Initializing syncProtocol");
|
|
19
|
-
// Constants:
|
|
20
|
-
var RECONNECT_DELAY = 5000; // Reconnect delay in case of errors such as network down.
|
|
21
|
-
var TOKEN_REFRESH_BUFFER = 60; // Refresh token this many seconds before exp
|
|
22
|
-
|
|
23
|
-
Dexie.Syncable.registerSyncProtocol("websocket", {
|
|
24
|
-
sync: function (
|
|
25
|
-
context,
|
|
26
|
-
url,
|
|
27
|
-
options,
|
|
28
|
-
baseRevision,
|
|
29
|
-
syncedRevision,
|
|
30
|
-
changes,
|
|
31
|
-
partial,
|
|
32
|
-
applyRemoteChanges,
|
|
33
|
-
onChangesAccepted,
|
|
34
|
-
onSuccess,
|
|
35
|
-
onError,
|
|
36
|
-
) {
|
|
37
|
-
// The following vars are needed because we must know which callback to ack when server sends it's ack to us.
|
|
38
|
-
var requestId = 0;
|
|
39
|
-
var acceptCallbacks = {};
|
|
40
|
-
var refreshTimer = null;
|
|
41
|
-
var pendingTokenUpdate = null;
|
|
42
|
-
|
|
43
|
-
// Connect the WebSocket to given url:
|
|
44
|
-
log("Connecting to", url)
|
|
45
|
-
var ws = new WebSocket(url);
|
|
46
|
-
|
|
47
|
-
// sendChanges() method:
|
|
48
|
-
function sendChanges(changes, baseRevision, partial, onChangesAccepted) {
|
|
49
|
-
log("sendChanges", changes.length, baseRevision);
|
|
50
|
-
++requestId;
|
|
51
|
-
acceptCallbacks[requestId.toString()] = onChangesAccepted;
|
|
52
|
-
|
|
53
|
-
// In this example, the server expects the following JSON format of the request:
|
|
54
|
-
// {
|
|
55
|
-
// type: "changes"
|
|
56
|
-
// baseRevision: baseRevision,
|
|
57
|
-
// changes: changes,
|
|
58
|
-
// partial: partial,
|
|
59
|
-
// requestId: id
|
|
60
|
-
// }
|
|
61
|
-
// To make the sample simplified, we assume the server has the exact same specification of how changes are structured.
|
|
62
|
-
// In real world, you would have to pre-process the changes array to fit the server specification.
|
|
63
|
-
// However, this example shows how to deal with the WebSocket to fullfill the API.
|
|
64
|
-
|
|
65
|
-
ws.send(
|
|
66
|
-
JSON.stringify({
|
|
67
|
-
type: "changes",
|
|
68
|
-
changes: changes,
|
|
69
|
-
partial: partial,
|
|
70
|
-
baseRevision: baseRevision,
|
|
71
|
-
requestId: requestId,
|
|
72
|
-
}),
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
function clearRefreshTimer() {
|
|
79
|
-
if (refreshTimer) {
|
|
80
|
-
clearTimeout(refreshTimer);
|
|
81
|
-
refreshTimer = null;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function sendTokenUpdate(token) {
|
|
86
|
-
if (ws.readyState !== WebSocket.OPEN) return false;
|
|
87
|
-
pendingTokenUpdate = token;
|
|
88
|
-
ws.send(JSON.stringify({ type: "tokenUpdate", authToken: token }));
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Resolve the getToken function from the module-level registry.
|
|
93
|
-
// It's stored there (not in options) because dexie-syncable serializes
|
|
94
|
-
// options into IndexedDB, and functions can't survive structured clone.
|
|
95
|
-
function resolveGetToken() {
|
|
96
|
-
var fn = getTokenGetter(url);
|
|
97
|
-
if (!fn) throw new Error("No token getter registered for " + url);
|
|
98
|
-
return fn;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Schedule a proactive token refresh before the JWT expires.
|
|
102
|
-
// Sends a tokenUpdate message on the existing WebSocket so the
|
|
103
|
-
// server can accept the new token without dropping the connection.
|
|
104
|
-
function scheduleTokenRefresh(tokenStr) {
|
|
105
|
-
clearRefreshTimer();
|
|
106
|
-
var exp = decodeJwtExp(tokenStr);
|
|
107
|
-
if (!exp) return;
|
|
108
|
-
var msUntilRefresh = (exp - TOKEN_REFRESH_BUFFER) * 1000 - Date.now();
|
|
109
|
-
if (msUntilRefresh <= 0) return;
|
|
110
|
-
log("Scheduling proactive token refresh in", Math.round(msUntilRefresh / 1000), "s");
|
|
111
|
-
refreshTimer = setTimeout(async function () {
|
|
112
|
-
try {
|
|
113
|
-
var newToken = await resolveGetToken()({ forceRefresh: true });
|
|
114
|
-
if (sendTokenUpdate(newToken)) {
|
|
115
|
-
log("Sending tokenUpdate on existing WebSocket");
|
|
116
|
-
}
|
|
117
|
-
} catch (err) {
|
|
118
|
-
log("Proactive token refresh failed (non-fatal):", err);
|
|
119
|
-
}
|
|
120
|
-
}, msUntilRefresh);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// When WebSocket opens, get a fresh token and send our identity to the server.
|
|
124
|
-
// This runs on every open, including reconnects after ERROR_WILL_RETRY,
|
|
125
|
-
// so each attempt gets a fresh token via getToken().
|
|
126
|
-
ws.onopen = async function (event) {
|
|
127
|
-
try {
|
|
128
|
-
var token = await resolveGetToken()();
|
|
129
|
-
log("Opening socket - sending clientIdentity", context.clientIdentity);
|
|
130
|
-
ws.send(
|
|
131
|
-
JSON.stringify({
|
|
132
|
-
type: "clientIdentity",
|
|
133
|
-
clientIdentity: context.clientIdentity || null,
|
|
134
|
-
authToken: token,
|
|
135
|
-
schema: options.schema
|
|
136
|
-
}),
|
|
137
|
-
);
|
|
138
|
-
scheduleTokenRefresh(token);
|
|
139
|
-
} catch (err) {
|
|
140
|
-
log("Failed to get token for WebSocket:", err);
|
|
141
|
-
ws.close();
|
|
142
|
-
onError("Authentication failed: " + (err.message || err), RECONNECT_DELAY);
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
// When the page becomes visible again (e.g. PWA/mobile browser resuming
|
|
147
|
-
// from background), the scheduled setTimeout for token refresh may have
|
|
148
|
-
// been frozen by the browser. Force-refresh the token and re-send it to
|
|
149
|
-
// the server so the WebSocket connection stays authenticated.
|
|
150
|
-
function handleVisibilityResume() {
|
|
151
|
-
if (document.visibilityState === 'visible' && ws.readyState === WebSocket.OPEN) {
|
|
152
|
-
log("Page became visible - refreshing token for WebSocket");
|
|
153
|
-
resolveGetToken()({ forceRefresh: true }).then(function(newToken) {
|
|
154
|
-
sendTokenUpdate(newToken);
|
|
155
|
-
}).catch(function(err) {
|
|
156
|
-
log("Token refresh on visibility resume failed:", err);
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (typeof document !== 'undefined') {
|
|
161
|
-
document.addEventListener('visibilitychange', handleVisibilityResume);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function cleanupVisibilityListener() {
|
|
165
|
-
if (typeof document !== 'undefined') {
|
|
166
|
-
document.removeEventListener('visibilitychange', handleVisibilityResume);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// If network down or other error, tell the framework to reconnect again in some time:
|
|
171
|
-
ws.onerror = function (event) {
|
|
172
|
-
clearRefreshTimer();
|
|
173
|
-
cleanupVisibilityListener();
|
|
174
|
-
ws.close();
|
|
175
|
-
log("ws.onerror", event);
|
|
176
|
-
onError(event?.message, RECONNECT_DELAY);
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
// If socket is closed (network disconnected), inform framework and make it reconnect
|
|
180
|
-
ws.onclose = function (event) {
|
|
181
|
-
clearRefreshTimer();
|
|
182
|
-
cleanupVisibilityListener();
|
|
183
|
-
onError("Socket closed: " + event.reason, RECONNECT_DELAY);
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
// isFirstRound: Will need to call onSuccess() only when we are in sync the first time.
|
|
187
|
-
// onSuccess() will unblock Dexie to be used by application code.
|
|
188
|
-
// If for example app code writes: db.friends.where('shoeSize').above(40).toArray(callback), the execution of that query
|
|
189
|
-
// will not run until we have called onSuccess(). This is because we want application code to get results that are as
|
|
190
|
-
// accurate as possible. Specifically when connected the first time and the entire DB is being synced down to the browser,
|
|
191
|
-
// it is important that queries starts running first when db is in sync.
|
|
192
|
-
var isFirstRound = true;
|
|
193
|
-
// When message arrive from the server, deal with the message accordingly:
|
|
194
|
-
ws.onmessage = function (event) {
|
|
195
|
-
try {
|
|
196
|
-
// Assume we have a server that should send JSON messages of the following format:
|
|
197
|
-
// {
|
|
198
|
-
// type: "clientIdentity", "changes", "ack" or "error"
|
|
199
|
-
// clientIdentity: unique value for our database client node to persist in the context. (Only applicable if type="clientIdentity")
|
|
200
|
-
// message: Error message (Only applicable if type="error")
|
|
201
|
-
// requestId: ID of change request that is acked by the server (Only applicable if type="ack" or "error")
|
|
202
|
-
// changes: changes from server (Only applicable if type="changes")
|
|
203
|
-
// lastRevision: last revision of changes sent (applicable if type="changes")
|
|
204
|
-
// partial: true if server has additionalChanges to send. False if these changes were the last known. (applicable if type="changes")
|
|
205
|
-
// }
|
|
206
|
-
var requestFromServer = JSON.parse(event.data);
|
|
207
|
-
log("requestFromServer", requestFromServer, { isFirstRound });
|
|
208
|
-
|
|
209
|
-
if (requestFromServer.type == "clientIdentity") {
|
|
210
|
-
context.clientIdentity = requestFromServer.clientIdentity;
|
|
211
|
-
context.save();
|
|
212
|
-
|
|
213
|
-
sendChanges(changes, baseRevision, partial, onChangesAccepted);
|
|
214
|
-
|
|
215
|
-
ws.send(
|
|
216
|
-
JSON.stringify({
|
|
217
|
-
type: "subscribe",
|
|
218
|
-
syncedRevision: syncedRevision,
|
|
219
|
-
}),
|
|
220
|
-
);
|
|
221
|
-
} else if (requestFromServer.type == "changes") {
|
|
222
|
-
applyRemoteChanges(
|
|
223
|
-
requestFromServer.changes,
|
|
224
|
-
requestFromServer.currentRevision,
|
|
225
|
-
requestFromServer.partial,
|
|
226
|
-
);
|
|
227
|
-
if (isFirstRound && !requestFromServer.partial) {
|
|
228
|
-
// Since this is the first sync round and server sais we've got all changes - now is the time to call onsuccess()
|
|
229
|
-
onSuccess({
|
|
230
|
-
// Specify a react function that will react on additional client changes
|
|
231
|
-
react: function (
|
|
232
|
-
changes,
|
|
233
|
-
baseRevision,
|
|
234
|
-
partial,
|
|
235
|
-
onChangesAccepted,
|
|
236
|
-
) {
|
|
237
|
-
sendChanges(
|
|
238
|
-
changes,
|
|
239
|
-
baseRevision,
|
|
240
|
-
partial,
|
|
241
|
-
onChangesAccepted,
|
|
242
|
-
);
|
|
243
|
-
},
|
|
244
|
-
disconnect: function () {
|
|
245
|
-
clearRefreshTimer();
|
|
246
|
-
cleanupVisibilityListener();
|
|
247
|
-
ws.close();
|
|
248
|
-
},
|
|
249
|
-
});
|
|
250
|
-
isFirstRound = false;
|
|
251
|
-
}
|
|
252
|
-
} else if (requestFromServer.type == "tokenUpdateAck") {
|
|
253
|
-
if (requestFromServer.ok) {
|
|
254
|
-
scheduleTokenRefresh(requestFromServer.authToken || pendingTokenUpdate);
|
|
255
|
-
pendingTokenUpdate = null;
|
|
256
|
-
} else {
|
|
257
|
-
log("tokenUpdate rejected by server:", requestFromServer.code || requestFromServer.message);
|
|
258
|
-
pendingTokenUpdate = null;
|
|
259
|
-
ws.close(4001, requestFromServer.code || "token_update_failed");
|
|
260
|
-
onError(
|
|
261
|
-
requestFromServer.message || "Authentication refresh failed",
|
|
262
|
-
RECONNECT_DELAY,
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
} else if (requestFromServer.type == "ack") {
|
|
266
|
-
var requestId = requestFromServer.requestId;
|
|
267
|
-
var acceptCallback = acceptCallbacks[requestId.toString()];
|
|
268
|
-
acceptCallback(); // Tell framework that server has acknowledged the changes sent.
|
|
269
|
-
delete acceptCallbacks[requestId.toString()];
|
|
270
|
-
} else if (requestFromServer.type == "error") {
|
|
271
|
-
ws.close();
|
|
272
|
-
if (requestFromServer.code === "TOKEN_EXPIRED" || requestFromServer.code === "UNAUTHORIZED") {
|
|
273
|
-
log("Auth error from server, will reconnect with fresh token:", requestFromServer.message);
|
|
274
|
-
onError(requestFromServer.message, RECONNECT_DELAY);
|
|
275
|
-
} else {
|
|
276
|
-
onError(requestFromServer.message, Infinity);
|
|
277
|
-
}
|
|
278
|
-
} else {
|
|
279
|
-
log("unknown message", requestFromServer);
|
|
280
|
-
ws.close();
|
|
281
|
-
onError("unknown message", Infinity);
|
|
282
|
-
}
|
|
283
|
-
} catch (e) {
|
|
284
|
-
ws.close();
|
|
285
|
-
log("caught error", e)
|
|
286
|
-
onError(e, Infinity); // Something went crazy. Server sends invalid format or our code is buggy. Dont reconnect - it would continue failing.
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
},
|
|
290
|
-
});
|
|
291
|
-
};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Module-level registry for token getter functions, keyed by WebSocket URL.
|
|
3
|
-
*
|
|
4
|
-
* dexie-syncable serializes the `options` object into IndexedDB via
|
|
5
|
-
* structured clone, which cannot handle functions. This registry keeps
|
|
6
|
-
* the getToken function out of `options` so it survives serialization
|
|
7
|
-
* while remaining accessible to the sync protocol on every (re)connect.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
type GetTokenFn = (options?: { forceRefresh?: boolean }) => Promise<string>
|
|
11
|
-
|
|
12
|
-
const registry = new Map<string, GetTokenFn>()
|
|
13
|
-
|
|
14
|
-
export function setTokenGetter(url: string, fn: GetTokenFn): void {
|
|
15
|
-
registry.set(url, fn)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function getTokenGetter(url: string): GetTokenFn | undefined {
|
|
19
|
-
return registry.get(url)
|
|
20
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { BasicStorage } from '../utils/storage'
|
|
2
|
-
import { Migration } from './versionUpdater'
|
|
3
|
-
import { log } from '../config'
|
|
4
|
-
|
|
5
|
-
export const addMigrationTimestamp: Migration = {
|
|
6
|
-
fromVersion: '0.6.0',
|
|
7
|
-
toVersion: '0.7.0',
|
|
8
|
-
async migrate(storage: BasicStorage) {
|
|
9
|
-
log('Running migration 0.6.0 → 0.7.0')
|
|
10
|
-
storage.set('test_migration', 'true')
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Get all available migrations
|
|
17
|
-
*/
|
|
18
|
-
export function getMigrations(): Migration[] {
|
|
19
|
-
return [
|
|
20
|
-
addMigrationTimestamp
|
|
21
|
-
]
|
|
22
|
-
}
|