Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support yjs in dexie-cloud-addon #2045

Open
wants to merge 86 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
b56cee3
Using lib es2021 (for FinalizationRegistry) + installed yjs as devdep.
dfahlander Jun 10, 2024
24ac71f
Support for Y.js complete (dry impl)
dfahlander Jun 19, 2024
4cd9ac1
First yjs unit-test + bug fix
dfahlander Jun 19, 2024
0cce18f
Test DexieYProvider + bugfixes
dfahlander Jun 19, 2024
b2e86b0
trigger update deletion when main row is deleted
dfahlander Jun 19, 2024
ad6c737
Manipulate Y.js doc within a dexie transaction to avoid race condition
dfahlander Jun 19, 2024
6742cfa
Upgrading tests to use Firefox version 120
dfahlander Jun 19, 2024
60e83bc
Upgrading tests to FireFox 126
dfahlander Jun 19, 2024
1c4b5b0
For curiosity, downgrade Firefox to 125
dfahlander Jun 19, 2024
0e069f1
Support Firefox < 126: Don't use compoun indexes with auto-incremente…
dfahlander Jun 19, 2024
22d79b5
Revert to using FF118 again
dfahlander Jun 19, 2024
7742ebb
Comment
dfahlander Jul 1, 2024
1200a19
Merge branch 'master' into support-yjs
dfahlander Jul 1, 2024
60aef01
Merge branch 'master' into support-yjs
dfahlander Jul 10, 2024
27e13e4
Let db.on('close') fire also when db.close() is called.
dfahlander Jul 19, 2024
a3ef4b1
GC implemented and tested.
dfahlander Jul 22, 2024
3e32413
Finally a working flow
dfahlander Jul 23, 2024
16a04d0
Compression of updates complete and tested.
dfahlander Jul 23, 2024
5cb596a
Send and receive Y-updates in dexie-cloud-addon
dfahlander Jul 23, 2024
8995d85
Added Dexie.once in addition to Dexie.on. Handy example: db.once('clo…
dfahlander Jul 28, 2024
31ac76e
Update dts-bundle-generator.
dfahlander Jul 28, 2024
4ceb588
Refactored Y.js support:
dfahlander Jul 28, 2024
54ef0e7
Updated test
dfahlander Jul 28, 2024
c013c51
Made it possible to reach the Y.Doc cache from addons or outside dexie:
dfahlander Jul 28, 2024
d9e3f76
Y.js support in dexie-cloud-addon:
dfahlander Jul 28, 2024
d2cf903
Don't expose the WeakRef type in public API (would be a breaking typi…
dfahlander Jul 28, 2024
745c96f
Don't sync Y.Doc on tables not marked for sync.
dfahlander Jul 31, 2024
659f7a1
Bump version of dexie-cloud-common
dfahlander Jul 31, 2024
aa38a76
Refactor: use table+prop instead of updatesTable because:
dfahlander Jul 31, 2024
41e113a
Upgraded pnpm to 9
Aug 23, 2024
878be8c
Upgrade pnpm in workflow
Aug 23, 2024
c501020
Bump version to alpha
Aug 23, 2024
7ec8ec5
Make pnpm not complain when dexie is in alpha or beta
Aug 26, 2024
2852116
useYProvider hook
Aug 26, 2024
a0299bb
Solving reference error.
Aug 26, 2024
5482ade
No need to double-destroy provider
Aug 28, 2024
b5c6a24
whitespace change only
Aug 28, 2024
26bb73d
Comment spelling
Sep 4, 2024
cc37806
Wrong call to updateTable.put() - it's inbound
Sep 4, 2024
50cd0a2
Add receivedUntil on YSyncState (formerly YSyncer) and respect it whe…
Sep 9, 2024
0591312
Updated Y sync flow:
Sep 9, 2024
ca17376
Bump version of dexie-cloud-common
Sep 9, 2024
75dae89
Refactor DexieYProvider and renamed
Sep 11, 2024
82be5ad
Complete client-side implementation of:
Sep 11, 2024
6a83b4e
Adjust to newer lib.dom.d.ts: IDBTransactionDurability seem to have b…
Sep 11, 2024
91b620b
Put back the YStateVector message (needed in normal sync)
Sep 11, 2024
2324e56
Bump version of dexie-cloud-common
Sep 11, 2024
5c05775
Send Yjs updates over binary websocket channel.
Sep 27, 2024
02b9c09
Utilities for consuming binary stream from fetch
Oct 2, 2024
9f39feb
async binary stream producer
Oct 3, 2024
d417e2d
Last piece in place for a completed YJS support in dexie-cloud-addon:
Oct 7, 2024
b818b3d
Bugfix by review: clearout collectedDocs after bulkAdd()
Oct 7, 2024
6d75a83
Missing break
Oct 7, 2024
76b4dc9
dexie-cloud-addon version bump
Oct 7, 2024
f38451e
Bugfixes after testing:
Oct 9, 2024
01ca6ea
Fix nullish check
Oct 9, 2024
41332bf
Don't push y-updates or awareness or doc-open before logging in
Oct 10, 2024
0a10bd9
Respect cloud option disableEagerSync in regards to avoid pushing Y m…
Oct 10, 2024
13116a5
Version bump on 3 libraries: dexie, dexie-cloud-addon and dexie-react…
Oct 11, 2024
92ba86a
Support providing initial Y.Doc when adding new rows
Oct 13, 2024
45669c1
Version bump
Oct 13, 2024
3357ccb
Don't GC every 10 seconds. Instead:
Oct 13, 2024
17e9c1b
Version bump
Oct 13, 2024
3166136
Fix: MissingAPIError("awarenessProtocol was not provided...") when Yj…
Oct 14, 2024
4dbcc43
Make useDocument() not throw with non-dexie Y.Doc instances.
Oct 14, 2024
44e392f
Version bump
Oct 14, 2024
5164b0d
Version bump
Oct 14, 2024
e328321
Bugfix for DexieYProvider.release() - it unconditionally destroyed do…
Oct 16, 2024
ccd1c2b
Make it possible to opt-out from Y.js garbage collection (for debugging)
Oct 16, 2024
e80b7dd
Debug printouts for Y messages
Oct 16, 2024
92542c7
Bugfix: some YJS updates were never sent to server
Oct 16, 2024
addac96
Don't open docs until awarenss is requested.
Oct 16, 2024
db33099
Possible bug: could theoretically miss y-updates in DexieYProvider
Oct 16, 2024
eefba6f
When rejecting an update, reject all following updates as well from t…
Oct 16, 2024
7dd40eb
Feature: defineYDocTrigger(). Should be moved to another addon, but t…
Oct 16, 2024
d8f9263
Version bumps
Oct 16, 2024
da98d8f
Dexie Cloud: Support for unsynced properties (local-only properties t…
Oct 17, 2024
582530a
Version bump
Oct 17, 2024
e20351d
Bugfix: Earlier change that omitted eager sync for changes that only …
Nov 26, 2024
efc5860
Avoid infinite circular mapped types for keypaths of entities with Y.…
Nov 26, 2024
8694105
Bugfixed yjs support in dexie + dexie-cloud-server using dexie-starte…
Nov 27, 2024
4d50f6b
Revert change belonging to strongly-typed-schema branch
Nov 27, 2024
d73c4d9
Bump package versions to 4.1.0-alpha.23
Nov 27, 2024
2120ab0
Merge branch 'master' into support-yjs-dexie-cloud
Nov 27, 2024
04c398e
Make bash is run with -e flag
Nov 27, 2024
572a350
Comment out tests that belongs to strongly-typed-schema branch
Nov 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,5 @@ jobs:
- name: Run headless test
uses: coactions/setup-xvfb@v1
with:
run: bash ./gh-actions.sh
run: bash -e ./gh-actions.sh
working-directory: ${{ matrix.TF }}
4 changes: 2 additions & 2 deletions addons/Dexie.Observable/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@
},
"homepage": "https://dexie.org",
"peerDependencies": {
"dexie": "workspace:^3.0.2 || ^4.0.1"
"dexie": "workspace:^"
},
"devDependencies": {
"@types/node": "^18.11.18",
"dexie": "workspace:^3.0.2 || ^4.0.1",
"dexie": "workspace:^",
"eslint": "^7.27.0",
"just-build": "^0.9.24",
"qunit": "^2.9.2",
Expand Down
4 changes: 2 additions & 2 deletions addons/Dexie.Syncable/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@
},
"homepage": "https://dexie.org",
"peerDependencies": {
"dexie": "workspace:^3.0.2 || ^4.0.1",
"dexie": "workspace:^",
"dexie-observable": "workspace:^"
},
"devDependencies": {
"dexie": "workspace:^3.0.2 || ^4.0.1",
"dexie": "workspace:^",
"dexie-observable": "workspace:^",
"eslint": "^5.16.0",
"just-build": "^0.9.24",
Expand Down
9 changes: 5 additions & 4 deletions addons/dexie-cloud/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dexie-cloud-addon",
"version": "4.0.8",
"version": "4.1.0-alpha.23",
"description": "Dexie addon that syncs with to Dexie Cloud",
"main": "dist/umd/dexie-cloud-addon.js",
"type": "module",
Expand Down Expand Up @@ -78,23 +78,24 @@
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.5",
"@types/node": "^18.11.18",
"dexie-cloud-common": "workspace:^",
"dreambase-library": "^1.0.21",
"just-build": "*",
"karma": "*",
"karma-chrome-launcher": "*",
"karma-firefox-launcher": "*",
"karma-qunit": "*",
"lib0": "^0.2.97",
"preact": "*",
"rollup": "^4.1.4",
"terser": "^5.20.0",
"tslib": "*",
"typescript": "^5.3.3"
"typescript": "^5.6.3"
},
"dependencies": {
"dexie-cloud-common": "^1.0.31",
"rxjs": "^7.x"
},
"peerDependencies": {
"dexie": "^4.0.1"
"dexie": "workspace:^"
}
}
6 changes: 6 additions & 0 deletions addons/dexie-cloud/src/DexieCloudOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export interface DexieCloudOptions {
// not be synced with Dexie Cloud
unsyncedTables?: string[];

unsyncedProperties?: {
[tableName: string]: string[];
}

// By default Dexie Cloud will suffix the cloud DB ID to your IndexedDB database name
// in order to ensure that the local database is uniquely tied to the remote one and
// will use another local database if databaseURL is changed or if dexieCloud addon
Expand All @@ -52,4 +56,6 @@ export interface DexieCloudOptions {
public_key: string;
hints?: { userId?: string; email?: string };
}) => Promise<TokenFinalResponse>;

awarenessProtocol?: typeof import('y-protocols/awareness');
}
1 change: 1 addition & 0 deletions addons/dexie-cloud/src/PermissionChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class PermissionChecker<T, TableNames extends string = TableName<T>> {
// If user can update any prop in any table in this realm, return true unless
// it regards to ownership change:
if (this.permissions.update === '*') {
// @ts-ignore
return props.every((prop) => prop !== 'owner');
}
const tablePermissions = this.permissions.update?.[this.tableName];
Expand Down
129 changes: 90 additions & 39 deletions addons/dexie-cloud/src/WSObservable.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { DBOperationsSet } from 'dexie-cloud-common';
import { BehaviorSubject, Observable, Subscriber, Subscription } from 'rxjs';
import { BehaviorSubject, Observable, Subscriber, Subscription, tap } from 'rxjs';
import { TokenExpiredError } from './authentication/TokenExpiredError';
import { DXCWebSocketStatus } from './DXCWebSocketStatus';
import { TSON } from './TSON';
import type { YClientMessage, YServerMessage } from 'dexie-cloud-common';
import { DexieCloudDB } from './db/DexieCloudDB';
import { createYClientUpdateObservable } from './yjs/createYClientUpdateObservable';
import { applyYServerMessages } from './yjs/applyYMessages';
import { DexieYProvider } from 'dexie';
import { getAwarenessLibrary, getDocAwareness } from './yjs/awareness';
import { encodeYMessage, decodeYMessage } from 'dexie-cloud-common';
import { UserLogin } from './dexie-cloud-client';
import { isEagerSyncDisabled } from './isEagerSyncDisabled';
import { getOpenDocSignal } from './yjs/reopenDocSignal';

const SERVER_PING_TIMEOUT = 20000;
const CLIENT_PING_INTERVAL = 30000;
const FAIL_RETRY_WAIT_TIME = 60000;

export type WSClientToServerMsg = ReadyForChangesMessage;
export type WSClientToServerMsg = ReadyForChangesMessage | YClientMessage;
export interface ReadyForChangesMessage {
type: 'ready';
realmSetHash: string;
Expand Down Expand Up @@ -73,24 +83,22 @@ export interface TokenExpiredMessage {

export class WSObservable extends Observable<WSConnectionMsg> {
constructor(
databaseUrl: string,
db: DexieCloudDB,
rev: string,
realmSetHash: string,
clientIdentity: string,
messageProducer: Observable<WSClientToServerMsg>,
webSocketStatus: BehaviorSubject<DXCWebSocketStatus>,
token?: string,
tokenExpiration?: Date
user: UserLogin
) {
super(
(subscriber) =>
new WSConnection(
databaseUrl,
db,
rev,
realmSetHash,
clientIdentity,
token,
tokenExpiration,
user,
subscriber,
messageProducer,
webSocketStatus
Expand All @@ -102,6 +110,7 @@ export class WSObservable extends Observable<WSConnectionMsg> {
let counter = 0;

export class WSConnection extends Subscription {
db: DexieCloudDB;
ws: WebSocket | null;
lastServerActivity: Date;
lastUserActivity: Date;
Expand All @@ -110,24 +119,22 @@ export class WSConnection extends Subscription {
rev: string;
realmSetHash: string;
clientIdentity: string;
token: string | undefined;
tokenExpiration: Date | undefined;
user: UserLogin;
subscriber: Subscriber<WSConnectionMsg>;
pauseUntil?: Date;
messageProducer: Observable<WSClientToServerMsg>;
webSocketStatus: BehaviorSubject<DXCWebSocketStatus>;
id = ++counter;

private pinger: any;
private messageProducerSubscription: null | Subscription;
private subscriptions: Set<Subscription> = new Set();

constructor(
databaseUrl: string,
db: DexieCloudDB,
rev: string,
realmSetHash: string,
clientIdentity: string,
token: string | undefined,
tokenExpiration: Date | undefined,
user: UserLogin,
subscriber: Subscriber<WSConnectionMsg>,
messageProducer: Observable<WSClientToServerMsg>,
webSocketStatus: BehaviorSubject<DXCWebSocketStatus>
Expand All @@ -136,18 +143,17 @@ export class WSConnection extends Subscription {
console.debug(
'New WebSocket Connection',
this.id,
token ? 'authorized' : 'unauthorized'
user.accessToken ? 'authorized' : 'unauthorized'
);
this.databaseUrl = databaseUrl;
this.db = db;
this.databaseUrl = db.cloud.options!.databaseUrl;
this.rev = rev;
this.realmSetHash = realmSetHash;
this.clientIdentity = clientIdentity;
this.token = token;
this.tokenExpiration = tokenExpiration;
this.user = user;
this.subscriber = subscriber;
this.lastUserActivity = new Date();
this.messageProducer = messageProducer;
this.messageProducerSubscription = null;
this.webSocketStatus = webSocketStatus;
this.connect();
}
Expand All @@ -169,10 +175,10 @@ export class WSConnection extends Subscription {
} catch {}
}
this.ws = null;
if (this.messageProducerSubscription) {
this.messageProducerSubscription.unsubscribe();
this.messageProducerSubscription = null;
for (const sub of this.subscriptions) {
sub.unsubscribe();
}
this.subscriptions.clear();
}

reconnecting = false;
Expand Down Expand Up @@ -205,7 +211,8 @@ export class WSConnection extends Subscription {
//console.debug('SyncStatus: DUBB: Ooops it was closed!');
return;
}
if (this.tokenExpiration && this.tokenExpiration < new Date()) {
const tokenExpiration = this.user.accessTokenExpiration;
if (tokenExpiration && tokenExpiration < new Date()) {
this.subscriber.error(new TokenExpiredError()); // Will be handled in connectWebSocket.ts.
return;
}
Expand Down Expand Up @@ -266,14 +273,14 @@ export class WSConnection extends Subscription {
searchParams.set('rev', this.rev);
searchParams.set('realmsHash', this.realmSetHash);
searchParams.set('clientId', this.clientIdentity);
if (this.token) {
searchParams.set('token', this.token);
if (this.user.accessToken) {
searchParams.set('token', this.user.accessToken);
}

// Connect the WebSocket to given url:
console.debug('dexie-cloud WebSocket create');
const ws = (this.ws = new WebSocket(`${wsUrl}/changes?${searchParams}`));
//ws.binaryType = "arraybuffer"; // For future when subscribing to actual changes.
ws.binaryType = "arraybuffer";

ws.onclose = (event: Event) => {
if (!this.pinger) return;
Expand All @@ -283,21 +290,47 @@ export class WSConnection extends Subscription {

ws.onmessage = (event: MessageEvent) => {
if (!this.pinger) return;
console.debug('dexie-cloud WebSocket onmessage', event.data);

this.lastServerActivity = new Date();
try {
const msg = TSON.parse(event.data) as
| WSConnectionMsg
| PongMessage
| ErrorMessage;
const msg = typeof event.data === 'string'
? TSON.parse(event.data) as
| WSConnectionMsg
| PongMessage
| ErrorMessage
| YServerMessage
: decodeYMessage(new Uint8Array(event.data)) as
| YServerMessage;
console.debug('dexie-cloud WebSocket onmessage', msg.type, msg);
if (msg.type === 'error') {
throw new Error(`Error message from dexie-cloud: ${msg.error}`);
}
if (msg.type === 'rev') {
this.rev = msg.rev; // No meaning but seems reasonable.
}
if (msg.type !== 'pong') {
} else if (msg.type === 'aware') {
const docCache = DexieYProvider.getDocCache(this.db.dx);
const doc = docCache.find(msg.table, msg.k, msg.prop);
if (doc) {
const awareness = getDocAwareness(doc);
if (awareness) {
const awap = getAwarenessLibrary(this.db);
awap.applyAwarenessUpdate(
awareness,
msg.u,
'server',
);
}
}
} else if (msg.type === 'u-ack' || msg.type === 'u-reject' || msg.type === 'u-s' || msg.type === 'in-sync') {
applyYServerMessages([msg], this.db);
} else if (msg.type === 'doc-open') {
const docCache = DexieYProvider.getDocCache(this.db.dx);
const doc = docCache.find(msg.table, msg.k, msg.prop);
if (doc) {
getOpenDocSignal(doc).next(); // Make yHandler reopen the document on server.
}
} else if (msg.type === 'outdated-server-rev' || msg.type === 'y-complete-sync-done') {
// Won't happen but need this for typing.
throw new Error('Outdated server revision or y-complete-sync-done not expected over WebSocket - only in sync using fetch()');
} else if (msg.type !== 'pong') {
// Forward the request to our subscriber, wich is in messageFromServerQueue.ts (via connectWebSocket's subscribe() at the end!)
this.subscriber.next(msg);
}
} catch (e) {
Expand All @@ -324,7 +357,7 @@ export class WSConnection extends Subscription {
}
};
});
this.messageProducerSubscription = this.messageProducer.subscribe(
this.subscriptions.add(this.messageProducer.subscribe(
(msg) => {
if (!this.closed) {
if (
Expand All @@ -333,10 +366,28 @@ export class WSConnection extends Subscription {
) {
this.webSocketStatus.next('connected');
}
this.ws?.send(TSON.stringify(msg));
console.debug('dexie-cloud WebSocket send', msg.type, msg);
if (msg.type === 'ready') {
// Ok, we are certain to have stored everything up until revision msg.rev.
// Update this.rev in case of reconnect - remember where we were and don't just start over!
this.rev = msg.rev;
// ... and then send along the request to the server so it would also be updated!
this.ws?.send(TSON.stringify(msg));
} else {
// If it's not a "ready" message, it's an YMessage.
// YMessages can be sent binary encoded.
this.ws?.send(encodeYMessage(msg));
}
}
}
);
));
if (this.user.isLoggedIn && !isEagerSyncDisabled(this.db)) {
this.subscriptions.add(
createYClientUpdateObservable(this.db).subscribe(
this.db.messageProducer
)
);
}
} catch (error) {
this.pauseUntil = new Date(Date.now() + FAIL_RETRY_WAIT_TIME);
}
Expand Down
3 changes: 3 additions & 0 deletions addons/dexie-cloud/src/db/DexieCloudDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { BroadcastedAndLocalEvent } from '../helpers/BroadcastedAndLocalEvent';
import { SyncState, SyncStatePhase } from '../types/SyncState';
import { MessagesFromServerConsumer } from '../sync/messagesFromServerQueue';
import { YClientMessage } from 'dexie-cloud-common';

/*export interface DexieCloudDB extends Dexie {
table(name: string): Table<any, any>;
Expand Down Expand Up @@ -66,6 +67,7 @@ export interface DexieCloudDB extends DexieCloudDBBase {
setInitiallySynced(initiallySynced: boolean): void;
reconfigure(): void;
messageConsumer: MessagesFromServerConsumer;
messageProducer: Subject<YClientMessage>;
}

const wm = new WeakMap<object, DexieCloudDB>();
Expand Down Expand Up @@ -193,6 +195,7 @@ export function DexieCloudDB(dx: Dexie): DexieCloudDB {

Object.assign(db, helperMethods);
db.messageConsumer = MessagesFromServerConsumer(db);
db.messageProducer = new Subject<YClientMessage>();
wm.set(dx.cloud, db);
}
return db;
Expand Down
8 changes: 8 additions & 0 deletions addons/dexie-cloud/src/db/entities/PersistedSyncState.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface PersistedSyncState {
serverRevision?: any;
yServerRevision?: string;
latestRevisions: {
[tableName: string]: number
};
Expand All @@ -11,4 +12,11 @@ export interface PersistedSyncState {
syncedTables: string[];
timestamp?: Date;
error?: string;
yDownloadedRealms?: {
[realmId: string]: "*" | {
tbl: string;
prop: string;
key: any;
}
}
}
Loading