Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 13 additions & 10 deletions projects/kit/offline/src/lib/offline-replica-pull.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
type OfflineReplicaPullPage,
type OfflineReplicaPullRequest,
} from './offline-replica-puller';
import { OfflineReplicaPullService } from './offline-replica-pull.service';
import { OfflineReplicaPullService, OfflineReplicaSchemaMismatchError } from './offline-replica-pull.service';
import { OfflineReplicaMutationCoordinator } from './offline-replica-mutation-coordinator';
import { generatedCommandIdentity, generatedReplicaIdentity } from './offline-test-helpers';
import {
Expand Down Expand Up @@ -762,11 +762,18 @@ describe('OfflineReplicaPullService', () => {
await expect(repository.getReplicaCursor(scope)).resolves.toEqual({ ...scope, cursor: 'cursor-v0' });
});

it('schema mismatchはrejectしcursorを進めない', async () => {
it('schema mismatchはtyped errorでrejectしcursorを進めない', async () => {
await repository.transactReplica({ putCursors: [{ ...scope, cursor: 'cursor-v0' }] });
pull.mockResolvedValueOnce(page([itemChange(42, 'Created')], { nextCursor: 'cursor-v1', schemaVersion: 99, schemaHash: 'deadbeef' }));

await expect(service.pull(scope)).rejects.toThrow('Offline replica schema mismatch');
const rejection = service.pull(scope);
await expect(rejection).rejects.toBeInstanceOf(OfflineReplicaSchemaMismatchError);
await expect(rejection).rejects.toMatchObject({
code: OfflineReplicaSchemaMismatchError.code,
clientVersion: 1,
serverVersion: 99,
serverHash: 'deadbeef',
});
await expect(repository.getReplicaCursor(scope)).resolves.toEqual({ ...scope, cursor: 'cursor-v0' });
expect(await repository.getReplicaRows(scope, 'test_items')).toEqual([]);
});
Expand Down Expand Up @@ -1115,13 +1122,9 @@ describe('OfflineReplicaPullService', () => {
})),
});
pull.mockResolvedValueOnce(
page(
[
itemChange(42, 'Remote 42', { serverRevision: 9 }),
itemChange(43, 'Remote 43', { serverRevision: 10 }),
],
{ nextCursor: 'cursor-v2' },
),
page([itemChange(42, 'Remote 42', { serverRevision: 9 }), itemChange(43, 'Remote 43', { serverRevision: 10 })], {
nextCursor: 'cursor-v2',
}),
);

await service.pull(scope);
Expand Down
26 changes: 23 additions & 3 deletions projects/kit/offline/src/lib/offline-replica-pull.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,28 @@ type CollapsedOfflineReplicaChange = OfflineReplicaChange & {
collapsedOrdinal: number;
};

/**
* Pull handshake reported a replica schema version/hash that does not match the local Kit schema.
*
* Prefer `instanceof` (or {@link OfflineReplicaSchemaMismatchError.code}) over English message text.
*/
export class OfflineReplicaSchemaMismatchError extends Error {
/** Stable machine-readable discriminator for fatal pull classification (pre- and post-send). */
static readonly code = 'OFFLINE_REPLICA_SCHEMA_MISMATCH' as const;

readonly code = OfflineReplicaSchemaMismatchError.code;

constructor(
readonly clientVersion: number,
readonly clientHash: string,
readonly serverVersion: number,
readonly serverHash: string,
) {
super(`Offline replica schema mismatch: client=${clientVersion}/${clientHash}, server=${serverVersion}/${serverHash}.`);
this.name = 'OfflineReplicaSchemaMismatchError';
}
}

/** Pulls authoritative server deltas into one durable local replica partition. */
@Injectable({ providedIn: 'root' })
export class OfflineReplicaPullService {
Expand Down Expand Up @@ -456,9 +478,7 @@ export class OfflineReplicaPullService {

#assertHandshake(version: number, hash: string, expectedHash: string): void {
if (version !== this.#options.replicaSchema.version || hash !== expectedHash) {
throw new Error(
`Offline replica schema mismatch: client=${this.#options.replicaSchema.version}/${expectedHash}, server=${version}/${hash}.`,
);
throw new OfflineReplicaSchemaMismatchError(this.#options.replicaSchema.version, expectedHash, version, hash);
}
}

Expand Down
Loading