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
2 changes: 1 addition & 1 deletion projects/kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ declare dummy adapters.
Mutations may still call `OfflineSyncService.enqueue` explicitly. A product that must keep ordinary HTTP services
offline-unaware can instead register `mutationPolicies`; a matched `POST` / `PUT` / `PATCH` / `DELETE` is prepared
locally before transport and returns an optimistic response while the Outbox owns remote replay.
Web storage uses Ionic Storage; iOS and Android use encrypted `@capacitor-community/sqlite`. Importing either the
Web storage uses Ionic Storage and supports `readCacheOnly` only. `synchronized` fails fast on Web because the current repository has no cross-tab lock; iOS and Android use encrypted `@capacitor-community/sqlite`. Importing either the
primary entry point or `/offline` does not pull the optional native SQLite plugin into web-only applications.

For cold-start offline route access, `OfflineCoordinatorService.activateOfflineSession()` restores only a manifest
Expand Down
14 changes: 13 additions & 1 deletion projects/kit/offline/src/lib/offline-contract.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { requirePositiveOfflineInteger } from './offline-identity';
import { provideOffline } from './offline-provider';
import { assertSupportedOfflineMode, provideOffline } from './offline-provider';
import { normalizeOfflineReplicaPullPage } from './offline-replica-puller';
import { defineOfflineReplicaSchema } from './offline-replica-schema';
import { offlineSessionManifestAllows } from './offline-session.service';
Expand Down Expand Up @@ -75,6 +75,18 @@ describe('shared offline boundary contracts', () => {
void compileOnly;
});

it('fails closed before enabling synchronized Outbox writes on Web', () => {
expect(() => assertSupportedOfflineMode('web', 'synchronized')).toThrow(
'Offline synchronized mode is supported only by native repositories.',
);
expect(() => assertSupportedOfflineMode('web', 'readCacheOnly')).not.toThrow();
expect(() => assertSupportedOfflineMode('electron', 'synchronized')).toThrow(
'Offline synchronized mode is supported only by native repositories.',
);
expect(() => assertSupportedOfflineMode('ios', 'synchronized')).not.toThrow();
expect(() => assertSupportedOfflineMode('android', 'synchronized')).not.toThrow();
});

it('requires an encryption key factory for synchronized providers at compile time', () => {
type Options = import('./offline-provider').ProvideSynchronizedOfflineOptions;
type IsRequired = Record<string, never> extends Pick<Options, 'createEncryptionKey'> ? false : true;
Expand Down
24 changes: 22 additions & 2 deletions projects/kit/offline/src/lib/offline-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import { OFFLINE_COMMAND_HOOKS } from './offline-command-hooks';
import type { OfflineKitOptions } from './offline-kit-options';
import { OFFLINE_KIT_OPTIONS } from './offline-kit-options';
import { OfflineCoordinatorService } from './offline-coordinator.service';
import { IonicOfflineRepository, OFFLINE_REPOSITORY, selectOfflineRepository } from './offline-repository';
import {
IonicOfflineRepository,
OFFLINE_REPOSITORY,
selectOfflineRepository,
supportsSynchronizedOfflineRepository,
} from './offline-repository';
import type { OfflineMutationRequestPolicy, OfflineRequestPolicy } from './offline-request-policy';
import { provideOfflineMutationRequestPolicy, provideOfflineRequestPolicy } from './offline-request-policy';
import type { OfflineReplicaProjector, OfflineReplicaPuller } from './offline-replica-puller';
Expand Down Expand Up @@ -121,6 +126,21 @@ export function provideOffline(options: ProvideOfflineOptions): EnvironmentProvi
...options.requestPolicies.flatMap((policy) => provideOfflineRequestPolicy(policy)),
...(options.mutationPolicies ?? []).flatMap((policy) => provideOfflineMutationRequestPolicy(policy)),
...(options.providers ?? []),
provideAppInitializer(() => inject(OfflineCoordinatorService).initialize()),
provideAppInitializer(() => {
assertSupportedOfflineMode(Capacitor.getPlatform(), options.mode ?? 'synchronized');
return inject(OfflineCoordinatorService).initialize();
}),
]);
}

/** Prevents unsupported multi-tab Web writes from silently losing Outbox state. */
export function assertSupportedOfflineMode(
platform: string,
mode: 'synchronized' | 'readCacheOnly',
): void {
if (!supportsSynchronizedOfflineRepository(platform) && mode === 'synchronized') {
throw new Error(
'Offline synchronized mode is supported only by native repositories. Use readCacheOnly until the selected repository provides cross-context locking.',
);
}
}
9 changes: 7 additions & 2 deletions projects/kit/offline/src/lib/offline-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,13 +202,18 @@ export interface OfflineRepository {
/** DI token for the selected platform repository. */
export const OFFLINE_REPOSITORY = new InjectionToken<OfflineRepository>('OFFLINE_REPOSITORY');

/** Selects encrypted SQLite on native platforms and Ionic Storage on web. */
/** Returns whether the platform uses the cross-process-safe native repository. */
export function supportsSynchronizedOfflineRepository(platform: string): boolean {
return platform === 'ios' || platform === 'android';
}

/** Selects encrypted SQLite on native platforms and Ionic Storage elsewhere. */
export function selectOfflineRepository(
platform: string,
webRepository: OfflineRepository,
nativeRepository: OfflineRepository,
): OfflineRepository {
return platform === 'ios' || platform === 'android' ? nativeRepository : webRepository;
return supportsSynchronizedOfflineRepository(platform) ? nativeRepository : webRepository;
}

interface OfflineMetadata {
Expand Down