Offline support with increments peristed and restored to / from indexedb
parent
15d2942aaa
commit
040a57f56a
@ -1,66 +0,0 @@
|
|||||||
import type {
|
|
||||||
ChangesRepository,
|
|
||||||
CLIENT_CHANGE,
|
|
||||||
SERVER_CHANGE,
|
|
||||||
} from "../sync/protocol";
|
|
||||||
|
|
||||||
// CFDO: add senderId, possibly roomId as well
|
|
||||||
export class DurableChangesRepository implements ChangesRepository {
|
|
||||||
constructor(private storage: DurableObjectStorage) {
|
|
||||||
// #region DEV ONLY
|
|
||||||
// this.storage.sql.exec(`DROP TABLE IF EXISTS changes;`);
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
this.storage.sql.exec(`CREATE TABLE IF NOT EXISTS changes(
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
payload TEXT NOT NULL,
|
|
||||||
version INTEGER NOT NULL DEFAULT 1,
|
|
||||||
createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public saveAll = (changes: Array<CLIENT_CHANGE>) => {
|
|
||||||
return this.storage.transactionSync(() => {
|
|
||||||
const prevVersion = this.getLastVersion();
|
|
||||||
const nextVersion = prevVersion + changes.length;
|
|
||||||
|
|
||||||
// CFDO: in theory payload could contain array of changes, if we would need to optimize writes
|
|
||||||
for (const [index, change] of changes.entries()) {
|
|
||||||
const version = prevVersion + index + 1;
|
|
||||||
// unique id ensures that we don't acknowledge the same change twice
|
|
||||||
this.storage.sql.exec(
|
|
||||||
`INSERT INTO changes (id, payload, version) VALUES (?, ?, ?);`,
|
|
||||||
change.id,
|
|
||||||
JSON.stringify(change),
|
|
||||||
version,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanity check
|
|
||||||
if (nextVersion !== this.getLastVersion()) {
|
|
||||||
throw new Error(
|
|
||||||
`Expected last acknowledged version to be "${nextVersion}", but it is "${this.getLastVersion()}!"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.getSinceVersion(prevVersion);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public getSinceVersion = (version: number): Array<SERVER_CHANGE> => {
|
|
||||||
return this.storage.sql
|
|
||||||
.exec<SERVER_CHANGE>(
|
|
||||||
`SELECT id, payload, version FROM changes WHERE version > (?) ORDER BY version ASC;`,
|
|
||||||
version,
|
|
||||||
)
|
|
||||||
.toArray();
|
|
||||||
};
|
|
||||||
|
|
||||||
public getLastVersion = (): number => {
|
|
||||||
const result = this.storage.sql
|
|
||||||
.exec(`SELECT MAX(version) FROM changes;`)
|
|
||||||
.one();
|
|
||||||
|
|
||||||
return result ? Number(result["MAX(version)"]) : 0;
|
|
||||||
};
|
|
||||||
}
|
|
@ -0,0 +1,88 @@
|
|||||||
|
import type {
|
||||||
|
IncrementsRepository,
|
||||||
|
CLIENT_INCREMENT,
|
||||||
|
SERVER_INCREMENT,
|
||||||
|
} from "../sync/protocol";
|
||||||
|
|
||||||
|
// CFDO: add senderId, possibly roomId as well
|
||||||
|
export class DurableIncrementsRepository implements IncrementsRepository {
|
||||||
|
constructor(private storage: DurableObjectStorage) {
|
||||||
|
// #region DEV ONLY
|
||||||
|
this.storage.sql.exec(`DROP TABLE IF EXISTS increments;`);
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
this.storage.sql.exec(`CREATE TABLE IF NOT EXISTS increments(
|
||||||
|
version INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
id TEXT NOT NULL UNIQUE,
|
||||||
|
createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
payload TEXT
|
||||||
|
);`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public saveAll(increments: Array<CLIENT_INCREMENT>) {
|
||||||
|
return this.storage.transactionSync(() => {
|
||||||
|
const prevVersion = this.getLastVersion();
|
||||||
|
const acknowledged: Array<SERVER_INCREMENT> = [];
|
||||||
|
|
||||||
|
for (const increment of increments) {
|
||||||
|
try {
|
||||||
|
// unique id ensures that we don't acknowledge the same increment twice
|
||||||
|
this.storage.sql.exec(
|
||||||
|
`INSERT INTO increments (id, payload) VALUES (?, ?);`,
|
||||||
|
increment.id,
|
||||||
|
JSON.stringify(increment),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// check if the increment has been already acknowledged
|
||||||
|
// in case client for some reason did not receive acknowledgement
|
||||||
|
// and reconnected while the we still have the increment in the worker
|
||||||
|
// otherwise the client is doomed to full a restore
|
||||||
|
if (
|
||||||
|
e instanceof Error &&
|
||||||
|
e.message.includes(
|
||||||
|
"UNIQUE constraint failed: increments.id: SQLITE_CONSTRAINT",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
acknowledged.push(this.getById(increment.id));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// query the just added increments
|
||||||
|
acknowledged.push(...this.getSinceVersion(prevVersion));
|
||||||
|
|
||||||
|
return acknowledged;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSinceVersion(version: number): Array<SERVER_INCREMENT> {
|
||||||
|
// CFDO: for versioning we need deletions, but not for the "snapshot" update;
|
||||||
|
return this.storage.sql
|
||||||
|
.exec<SERVER_INCREMENT>(
|
||||||
|
`SELECT id, payload, version FROM increments WHERE version > (?) ORDER BY version, createdAt ASC;`,
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLastVersion(): number {
|
||||||
|
// CFDO: might be in memory to reduce number of rows read (or index on version at least, if btree affect rows read)
|
||||||
|
const result = this.storage.sql
|
||||||
|
.exec(`SELECT MAX(version) FROM increments;`)
|
||||||
|
.one();
|
||||||
|
|
||||||
|
return result ? Number(result["MAX(version)"]) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getById(id: string): SERVER_INCREMENT {
|
||||||
|
return this.storage.sql
|
||||||
|
.exec<SERVER_INCREMENT>(
|
||||||
|
`SELECT id, payload, version FROM increments WHERE id = (?)`,
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
.one();
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue