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