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
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class OfflineReplicaMutationCoordinator {
run<T>(operation: (repository: OfflineRepository) => Promise<T>): Promise<T> {
return this.#enqueue(() => {
if (!this.#repository) throw new Error('Offline repository is not configured.');
const atomicMutation = this.#repository?.[OFFLINE_REPOSITORY_ATOMIC_MUTATION];
const atomicMutation = this.#repository[OFFLINE_REPOSITORY_ATOMIC_MUTATION];
return atomicMutation ? (atomicMutation.call(this.#repository, operation) as Promise<T>) : operation(this.#repository);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1528,7 +1528,7 @@ describe('OfflineReplicaPullService', () => {

await expect(service.pull(scope)).rejects.toThrow('Replica remote id is immutable: current=42, incoming=43.');
await expect(
repository.getReplicaRowIncludingPendingDelete?.(scope, 'test_items', generatedCommandIdentity('019d-server-id-immutable')),
repository.getReplicaRowIncludingPendingDelete!(scope, 'test_items', generatedCommandIdentity('019d-server-id-immutable')),
).resolves.toMatchObject({
identity: { kind: 'generated', localId: '019d-server-id-immutable', remoteId: 42 },
visibility: 'pending_delete',
Expand Down
3 changes: 0 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 @@ -308,7 +308,6 @@ export class OfflineReplicaPullService {
{ putRows: projection?.putRows ?? [], removeRows: projection?.removeRows ?? [] },
]);
const rematerialized = await this.#rematerializePendingAggregates(
scope,
userCommands,
scopeCommands,
confirmedAndProjected,
Expand Down Expand Up @@ -488,7 +487,6 @@ export class OfflineReplicaPullService {
}

async #rematerializePendingAggregates(
scope: OfflineScope,
userCommands: readonly OfflineCommand[],
scopeCommands: readonly OfflineCommand[],
currentRows: { putRows: readonly OfflineReplicaRow[]; removeRows: readonly OfflineReplicaRowKey[] },
Expand Down Expand Up @@ -558,7 +556,6 @@ export class OfflineReplicaPullService {
putRows.push(...(mutations.putRows ?? []));
removeRows.push(...(mutations.removeRows ?? []));
}
void scope;
return { putRows, removeRows };
}

Expand Down
10 changes: 8 additions & 2 deletions projects/kit/offline/src/lib/offline-repository.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,7 @@ describe('IonicOfflineRepository', () => {
});
});

it('同一createdAtはcommandId昇順で決定的に並べる', async () => {
it('同一createdAtを決定的に並べ、deprecated command aliasの互換性を保つ', async () => {
const base: Omit<OfflineCommand, 'scopeId' | 'commandId' | 'createdAt'> = {
userId: 1,
aggregateType: 'test_items',
Expand All @@ -829,6 +829,12 @@ describe('IonicOfflineRepository', () => {
await repository.putCommand({ ...base, scopeId: '11', commandId: 'cmd-m', createdAt: 10 });
expect((await repository.getCommands({ userId: 1, scopeId: '10' })).map((item) => item.commandId)).toEqual(['cmd-a', 'cmd-z']);
expect((await repository.getCommandsForUser!(1)).map((item) => item.commandId)).toEqual(['cmd-a', 'cmd-m', 'cmd-z']);
await repository.replaceCommand({ ...base, scopeId: '10', commandId: 'cmd-z', createdAt: 10, payload: { updated: true } });
expect(await repository.getCommands({ userId: 1, scopeId: '10' })).toContainEqual(
expect.objectContaining({ commandId: 'cmd-z', payload: { updated: true } }),
);
await repository.removeCommand('cmd-z');
expect((await repository.getCommands({ userId: 1, scopeId: '10' })).map((item) => item.commandId)).toEqual(['cmd-a']);
});

it('legacy web outboxの送信中と複数回試行済みの最終失敗をcommit不明として安全側へnormalizeする', async () => {
Expand Down Expand Up @@ -1460,7 +1466,7 @@ describe('IonicOfflineRepository', () => {
});

describe('pull attentions', () => {
it('put/get/removeとtransactionでuser+scope attentionを永続化する', async () => {
it('deprecated aliasとtransactionでuser+scope attentionをput/get/removeする', async () => {
await repository.putPullAttention!({
userId: 1,
scopeId: '10',
Expand Down
34 changes: 14 additions & 20 deletions projects/kit/offline/src/lib/offline-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,11 +258,13 @@ export interface OfflineRepository {
getCommands(scope: OfflineScope): Promise<OfflineCommand[]>;
getCommandsForUser?(userId: OfflinePrincipalId): Promise<OfflineCommand[]>;
putCommand(command: OfflineCommand): Promise<void>;
/** @deprecated Use {@link transactReplica} with `putCommands`. */
replaceCommand(command: OfflineCommand): Promise<void>;
/** @deprecated Use {@link transactReplica} with `removeCommandIds`. */
removeCommand(commandId: string): Promise<void>;
/** Upserts a durable fatal-pull attention for user+scope. */
/** @deprecated Use {@link transactReplica} with `putPullAttentions`. */
putPullAttention?(attention: OfflinePullAttention): Promise<void>;
/** Removes a durable fatal-pull attention for user+scope when present. */
/** @deprecated Use {@link transactReplica} with `removePullAttentions`. */
removePullAttention?(scope: OfflineScope): Promise<void>;
clearUser(userId: OfflinePrincipalId): Promise<void>;
clearScope(scope: OfflineScope): Promise<void>;
Expand Down Expand Up @@ -592,32 +594,24 @@ export class IonicOfflineRepository implements OfflineRepository {
});
}

/** @deprecated Use {@link transactReplica} with `putCommands`. */
replaceCommand(command: OfflineCommand): Promise<void> {
return this.putCommand(command);
}

async removeCommand(commandId: string): Promise<void> {
await this.initialize();
await this.#mutateRecord<OfflineCommand>(OUTBOX_KEY, (commands) => {
delete commands[commandId];
return commands;
});
/** @deprecated Use {@link transactReplica} with `removeCommandIds`. */
removeCommand(commandId: string): Promise<void> {
return this.transactReplica({ removeCommandIds: [commandId] });
}

async putPullAttention(attention: OfflinePullAttention): Promise<void> {
await this.initialize();
await this.#mutateRecord<OfflinePullAttention>(PULL_ATTENTIONS_KEY, (attentions) => {
attentions[this.#cursorKey(attention)] = attention;
return attentions;
});
/** @deprecated Use {@link transactReplica} with `putPullAttentions`. */
putPullAttention(attention: OfflinePullAttention): Promise<void> {
return this.transactReplica({ putPullAttentions: [attention] });
}

async removePullAttention(scope: OfflineScope): Promise<void> {
await this.initialize();
await this.#mutateRecord<OfflinePullAttention>(PULL_ATTENTIONS_KEY, (attentions) => {
delete attentions[this.#cursorKey(scope)];
return attentions;
});
/** @deprecated Use {@link transactReplica} with `removePullAttentions`. */
removePullAttention(scope: OfflineScope): Promise<void> {
return this.transactReplica({ removePullAttentions: [scope] });
}

async clearUser(userId: OfflinePrincipalId): Promise<void> {
Expand Down
72 changes: 36 additions & 36 deletions projects/kit/offline/src/lib/offline-sync.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,14 +203,6 @@ describe('OfflineSyncService', () => {
return commands.filter((item) => item.userId === userId);
}),
putCommand,
replaceCommand: vi.fn(async (command: OfflineCommand) => {
commands = commands.filter((item) => item.commandId !== command.commandId);
commands.push(structuredClone(command));
commands.sort((left, right) => left.createdAt - right.createdAt);
}),
removeCommand: vi.fn(async (commandId: string) => {
commands = commands.filter((item) => item.commandId !== commandId);
}),
getReplicaRow: vi.fn(async (scope: OfflineScope, sourceKey: string, identity: OfflineReplicaAddress) => {
await beforeGetReplicaRow?.();
return (
Expand Down Expand Up @@ -279,15 +271,6 @@ describe('OfflineSyncService', () => {
getPullAttentions: vi.fn(async (userId: number) =>
pullAttentions.filter((attention) => attention.userId === userId).map((attention) => structuredClone(attention)),
),
putPullAttention: vi.fn(async (attention: OfflinePullAttention) => {
pullAttentions = pullAttentions.filter(
(candidate) => candidate.userId !== attention.userId || candidate.scopeId !== attention.scopeId,
);
pullAttentions.push(structuredClone(attention));
}),
removePullAttention: vi.fn(async (scope: OfflineScope) => {
pullAttentions = pullAttentions.filter((candidate) => candidate.userId !== scope.userId || candidate.scopeId !== scope.scopeId);
}),
clearUser: vi.fn(async (userId: number) => {
commands = commands.filter((item) => item.userId !== userId);
rows = rows.filter((item) => item.userId !== userId);
Expand Down Expand Up @@ -1656,8 +1639,6 @@ describe('OfflineSyncService', () => {
getCommands: vi.fn(async () => []),
getCommandsForUser: vi.fn(async () => []),
putCommand: vi.fn(),
replaceCommand: vi.fn(),
removeCommand: vi.fn(),
getReplicaRow: vi.fn(async () => null),
getReplicaRowIncludingPendingDelete: vi.fn(async () => null),
getReplicaRowByRemoteId: vi.fn(async () => null),
Expand Down Expand Up @@ -3856,21 +3837,51 @@ describe('OfflineSyncService', () => {
expect(JSON.stringify(service.pendingCommands()[1]?.payload)).toBe(JSON.stringify(second));
});

it('JSON外payloadをrejectする', async () => {
it.each([
['undefined', () => ({ value: undefined })],
['NaN', () => ({ value: Number.NaN })],
['Infinity', () => ({ value: Number.POSITIVE_INFINITY })],
[
'循環参照',
() => {
const value: Record<string, unknown> = {};
value['self'] = value;
return value;
},
],
['非plain object', () => ({ value: new Date(0) })],
['array hole', () => ({ value: Array(1) })],
])('JSON外payload(%s)をrejectする', async (_case, payload) => {
await expect(
service.enqueue(
{
scopeId: '10',
aggregateType: 'documents',
identity: { kind: 'generated', localId: '1' },
operation: 'documents.upsert',
payload: { value: undefined },
payload: payload(),
},
{ flush: false },
),
).rejects.toBeInstanceOf(OfflinePayloadValidationError);
});

it('JSON payload内の共有参照は循環参照として扱わない', async () => {
const shared = { value: 'shared' };
await expect(
service.enqueue(
{
scopeId: '10',
aggregateType: 'documents',
identity: { kind: 'generated', localId: 'shared-reference' },
operation: 'documents.upsert',
payload: { left: shared, right: shared },
},
{ flush: false },
),
).resolves.toEqual(expect.any(String));
});

it.each([
[401, 'blocked_auth', 'blocked_auth'],
[409, 'conflict', 'conflict'],
Expand Down Expand Up @@ -3973,13 +3984,6 @@ describe('OfflineSyncService', () => {
commands = commands.filter((item) => item.commandId !== command.commandId);
commands.push(structuredClone(command));
}),
replaceCommand: vi.fn(async (command: OfflineCommand) => {
commands = commands.filter((item) => item.commandId !== command.commandId);
commands.push(structuredClone(command));
}),
removeCommand: vi.fn(async (commandId: string) => {
commands = commands.filter((item) => item.commandId !== commandId);
}),
getReplicaRow: vi.fn(async (scope: OfflineScope, sourceKey: string, identity: OfflineReplicaAddress) => {
return (
rows.find((item) => {
Expand Down Expand Up @@ -5793,18 +5797,14 @@ describe('OfflineSyncService', () => {
commands.push(structuredClone(command));
commands.sort(compareCommands);
}),
replaceCommand: vi.fn(async (command: OfflineCommand) => {
commands = commands.filter((item) => item.commandId !== command.commandId);
commands.push(structuredClone(command));
commands.sort(compareCommands);
}),
removeCommand: vi.fn(async (commandId: string) => {
commands = commands.filter((item) => item.commandId !== commandId);
}),
getReplicaRow: vi.fn(async (scope: OfflineScope, sourceKey: string, identity: OfflineReplicaAddress) => {
const row = findReplicaRow(scope, sourceKey, identity);
return row ? projectReplicaRow(row, scope) : null;
}),
getReplicaRowIncludingPendingDelete: vi.fn(async (scope: OfflineScope, sourceKey: string, identity: OfflineReplicaAddress) => {
const row = findReplicaRow(scope, sourceKey, identity);
return row ? projectReplicaRow(row, scope) : null;
}),
getReplicaRowByRemoteId: vi.fn(async (scope: OfflineScope, sourceKey: string, remoteId: number) => {
const row = rows.find((item) => {
if (
Expand Down
Loading