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
29 changes: 28 additions & 1 deletion projects/kit/offline/src/lib/offline-command-executor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { InjectionToken } from '@angular/core';
import type { OfflineCommand, OfflineScope } from './offline-repository';
import type { OfflineCommand, OfflineOptimisticReplicaCompanion, OfflineReplicaRow, OfflineScope } from './offline-repository';
import type { OfflineCommandIdentity, OfflinePrincipalId, OfflineReplicaIdentity } from './offline-identity';
import type { OfflineGeneratedRemoteId, OfflineNaturalKey } from './offline-replica-schema';

Expand Down Expand Up @@ -31,6 +31,19 @@ export interface OfflineCommandExecutor {
/** Sends the command using `command.commandId` as its durable server-side idempotency key. */
execute(command: OfflineCommand, target: OfflineCommandTarget): Promise<OfflineCommandResult>;
withServerRevision(command: OfflineCommand, revision: string | number): OfflineCommand;
/**
* Reapplies a complete aggregate's pending intents to a newer confirmed
* value. Return null when any intent is revision-sensitive. The returned
* values correspond to the original FIFO command order. Kit alone updates
* command metadata; payload and idempotency identity remain immutable.
* Without this hook, revision changes conflict by default.
*/
rebasePendingCommands?(
commands: readonly OfflineCommand[],
confirmedValues: unknown,
revision: string | number,
companionRows: readonly OfflineReplicaRow[],
): OfflinePendingRebase | null | Promise<OfflinePendingRebase | null>;
/**
* Whether this transport error authoritatively proves that this idempotency
* key did not commit. Returning true may clear an ambiguity retained from an
Expand All @@ -44,6 +57,20 @@ export interface OfflineCommandExecutor {
withoutServerRevision?(command: OfflineCommand): OfflineCommand;
}

/** Product projection result after safely replaying pending intents onto a newer confirmed revision. */
export interface OfflinePendingRebase {
/** Recomputed projections in the original durable FIFO order. */
steps: readonly OfflinePendingRebaseStep[];
}

/** Projection state produced for one immutable durable command in FIFO order. */
export interface OfflinePendingRebaseStep {
/** Recomputed full aggregate value after this command's intent is applied. */
optimisticValue: unknown;
/** Same footprint as the original command, rematerialized from the new confirmed value. */
optimisticCompanions?: readonly OfflineOptimisticReplicaCompanion[];
}

/** DI token for the product-specific command transport adapter. */
export const OFFLINE_COMMAND_EXECUTOR = new InjectionToken<OfflineCommandExecutor>('OFFLINE_COMMAND_EXECUTOR');

Expand Down
9 changes: 5 additions & 4 deletions projects/kit/offline/src/lib/offline-contract.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { offlineSessionManifestAllows } from './offline-session.service';

describe('shared offline boundary contracts', () => {
it('normalizes database serverId exactly once at the pull transport boundary', () => {
expect(
normalizeOfflineReplicaPullPage({
const normalized = normalizeOfflineReplicaPullPage({
schemaVersion: 1,
schemaHash: 'hash',
changes: [
Expand All @@ -29,8 +28,9 @@ describe('shared offline boundary contracts', () => {
],
nextCursor: '2',
hasMore: false,
}).changes,
).toEqual([
rebaselineRequired: true,
});
expect(normalized.changes).toEqual([
{
sourceKey: 'items',
remoteId: 42,
Expand All @@ -46,6 +46,7 @@ describe('shared offline boundary contracts', () => {
deleted: true,
},
]);
expect(normalized.rebaselineRequired).toBe(true);
});

it('narrows positive database ids without accepting coercion or zero', () => {
Expand Down
9 changes: 7 additions & 2 deletions projects/kit/offline/src/lib/offline-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { OfflineCoordinatorService } from './offline-coordinator.service';
import { IonicOfflineRepository, OFFLINE_REPOSITORY, selectOfflineRepository } from './offline-repository';
import type { OfflineMutationRequestPolicy, OfflineRequestPolicy } from './offline-request-policy';
import { provideOfflineMutationRequestPolicy, provideOfflineRequestPolicy } from './offline-request-policy';
import type { OfflineReplicaPuller } from './offline-replica-puller';
import { OFFLINE_REPLICA_PULLER } from './offline-replica-puller';
import type { OfflineReplicaProjector, OfflineReplicaPuller } from './offline-replica-puller';
import { OFFLINE_REPLICA_PROJECTOR, OFFLINE_REPLICA_PULLER } from './offline-replica-puller';
import { OfflineSessionService } from './offline-session.service';
import {
COMMUNITY_SQLITE,
Expand All @@ -29,6 +29,8 @@ interface ProvideOfflineOptionsBase extends OfflineKitOptions {
commandHooks?: Type<OfflineCommandHooks>;
/** Optional additional providers required by product adapters. */
providers?: readonly Provider[];
/** Optional pure adapter for product-owned local-only projections applied inside the Kit pull transaction. */
replicaProjector?: Type<OfflineReplicaProjector>;
/** Application-installed `@capacitor-community/sqlite` connection. Required only on iOS and Android. */
sqliteConnection?: CommunitySqliteConnection;
}
Expand Down Expand Up @@ -109,6 +111,9 @@ export function provideOffline(options: ProvideOfflineOptions): EnvironmentProvi
? { provide: OFFLINE_REPLICA_PULLER, useExisting: options.replicaPuller }
: { provide: OFFLINE_REPLICA_PULLER, useValue: READ_CACHE_ONLY_REPLICA_PULLER },
...(options.commandHooks ? [options.commandHooks, { provide: OFFLINE_COMMAND_HOOKS, useExisting: options.commandHooks }] : []),
...(options.replicaProjector
? [options.replicaProjector, { provide: OFFLINE_REPLICA_PROJECTOR, useExisting: options.replicaProjector }]
: []),
...options.requestPolicies.flatMap((policy) => provideOfflineRequestPolicy(policy)),
...(options.mutationPolicies ?? []).flatMap((policy) => provideOfflineMutationRequestPolicy(policy)),
...(options.providers ?? []),
Expand Down
Loading