Skip to content

Commit

Permalink
Close #413
Browse files Browse the repository at this point in the history
  • Loading branch information
steida committed May 25, 2024
1 parent 8a3a02a commit e420fec
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 29 deletions.
9 changes: 9 additions & 0 deletions .changeset/curly-seals-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@evolu/common": minor
---

New API for working with Evolu instances

The functions `resetOwner` and `restoreOwner` automatically reload the app to ensure no user data remains in memory. The new option `reload` allows us to opt out of this default behavior. For that reason, both functions return a promise that can be used to provide custom UX. There is also a new `reloadApp` function to reload the app in a platform-specific way (e.g., browsers will reload all tabs with Evolu instances).

The `createEvolu` function has a new option, `mnemonic`. This option is useful for Evolu multitenancy when creating an Evolu instance with a predefined mnemonic. To create a mnemonic, use the new `createMnemonic` function.
30 changes: 16 additions & 14 deletions packages/evolu-common/src/Db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
timestampToString,
unsafeTimestampFromString,
} from "./Crdt.js";
import { Bip39, Mnemonic, NanoIdGenerator } from "./Crypto.js";
import { Mnemonic, NanoIdGenerator } from "./Crypto.js";
import { QueryPatches, makePatches } from "./Diff.js";
import {
EvoluError,
Expand Down Expand Up @@ -77,6 +77,7 @@ export interface Db {
onError: Callbacks["onError"],
onSyncStateChange: Callbacks["onSyncStateChange"],
onReceive: Callbacks["onReceive"],
mnemonic: Mnemonic | undefined,
) => Effect.Effect<
Owner,
| NotSupportedPlatformError
Expand Down Expand Up @@ -174,13 +175,12 @@ export class DbFactory extends Context.Tag("DbFactory")<
export const createDb: Effect.Effect<
Db,
never,
SqliteFactory | Bip39 | NanoIdGenerator | Time | SyncFactory | SyncLock
SqliteFactory | NanoIdGenerator | Time | SyncFactory | SyncLock
> = Effect.gen(function* () {
const { createSqlite } = yield* SqliteFactory;
const { createSync } = yield* SyncFactory;

const initContext = Context.empty().pipe(
Context.add(Bip39, yield* Bip39),
Context.add(NanoIdGenerator, yield* NanoIdGenerator),
Context.add(Time, yield* Time),
Context.add(SyncLock, yield* SyncLock),
Expand All @@ -189,14 +189,7 @@ export const createDb: Effect.Effect<
const afterInitContext =
yield* Deferred.make<
Context.Context<
| Bip39
| NanoIdGenerator
| Time
| SyncLock
| Sqlite
| Owner
| Sync
| Callbacks
NanoIdGenerator | Time | SyncLock | Sqlite | Owner | Sync | Callbacks
>
>();

Expand All @@ -212,7 +205,14 @@ export const createDb: Effect.Effect<
const queryRowsRef = yield* SynchronizedRef.make<QueryRowsMap>(new Map());

const db: Db = {
init: (schema, initialData, onError, onSyncStateChange, onReceive) =>
init: (
schema,
initialData,
onError,
onSyncStateChange,
onReceive,
mnemonic,
) =>
Effect.gen(function* () {
yield* Effect.logDebug(["Db init", { schema }]);
const sqlite = yield* createSqlite;
Expand All @@ -222,7 +222,9 @@ export const createDb: Effect.Effect<
Effect.flatMap((currentSchema) => {
if (currentSchema.tables.map((t) => t.name).includes("evolu_owner"))
return readOwner;
return createOwner().pipe(Effect.tap(applyMutations(initialData)));
return createOwner(mnemonic).pipe(
Effect.tap(applyMutations(initialData)),
);
}),
sqlite.transaction("exclusive"),
Effect.provide(contextWithSqlite),
Expand Down Expand Up @@ -526,7 +528,7 @@ const readOwner = Effect.logTrace("Db readOwner").pipe(
),
);

const createOwner = (mnemonic?: Mnemonic) =>
const createOwner = (mnemonic: Mnemonic | undefined) =>
Effect.logTrace("Db createOwner").pipe(
Effect.zipRight(
Effect.all([makeOwner(mnemonic), Sqlite, makeInitialTimestamp]),
Expand Down
69 changes: 54 additions & 15 deletions packages/evolu-common/src/Evolu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as Predicate from "effect/Predicate";
import * as Record from "effect/Record";
import * as Kysely from "kysely";
import { Config, createRuntime, defaultConfig } from "./Config.js";
import { TimestampString } from "./Crdt.js";
import { Mnemonic, NanoIdGenerator } from "./Crypto.js";
import {
DbFactory,
Expand Down Expand Up @@ -44,7 +45,6 @@ import {
} from "./Sqlite.js";
import { Listener, Unsubscribe, makeStore } from "./Store.js";
import { SyncState, initialSyncState } from "./Sync.js";
import { TimestampString } from "./Crdt.js";

/**
* The Evolu interface provides a type-safe SQL query building and state
Expand Down Expand Up @@ -341,11 +341,30 @@ export interface Evolu<T extends EvoluSchema = EvoluSchema> {
* Delete {@link Owner} and all their data from the current device. After the
* deletion, Evolu will purge the application state. For browsers, this will
* reload all tabs using Evolu. For native apps, it will restart the app.
*
* Reloading can be turned off via options if you want to provide a different
* UX.
*/
readonly resetOwner: (options?: {
readonly reload: boolean;
}) => Promise<void>;

/**
* Restore {@link Owner} with all their synced data. It uses {@link resetOwner},
* so be careful.
*/
readonly resetOwner: () => void;
readonly restoreOwner: (
mnemonic: Mnemonic,
options?: {
readonly reload: boolean;
},
) => Promise<void>;

/** Restore {@link Owner} with all their synced data. */
readonly restoreOwner: (mnemonic: Mnemonic) => void;
/**
* Reload the app in a platform-specific way. For browsers, this will reload
* all tabs using Evolu. For native apps, it will restart the app.
*/
readonly reloadApp: () => void;

/**
* Ensure tables and columns defined in {@link EvoluSchema} exist in the
Expand Down Expand Up @@ -485,7 +504,12 @@ export class EvoluFactory extends Context.Tag("EvoluFactory")<
return EvoluFactory.of({
createEvolu: <T extends EvoluSchema, I>(
schema: S.Schema<T, I>,
{ indexes, initialData, ...config }: Partial<EvoluConfig<T>> = {},
{
indexes,
initialData,
mnemonic,
...config
}: Partial<EvoluConfig<T>> = {},
): Evolu<T> => {
const runtime = createRuntime(config);
const name = config?.name || defaultConfig.name;
Expand All @@ -499,6 +523,7 @@ export class EvoluFactory extends Context.Tag("EvoluFactory")<
dbSchema,
runtime,
initialData as EvoluConfig["initialData"],
mnemonic,
).pipe(Effect.provide(context), runtime.runSync);
instances.set(name, evolu);
} else {
Expand All @@ -514,7 +539,7 @@ export class EvoluFactory extends Context.Tag("EvoluFactory")<
export interface EvoluConfig<T extends EvoluSchema = EvoluSchema>
extends Config {
/**
* Use the `indexes` property to define SQLite indexes.
* Use the `indexes` option to define SQLite indexes.
*
* Table and column names are not typed because Kysely doesn't support it.
*
Expand All @@ -533,6 +558,13 @@ export interface EvoluConfig<T extends EvoluSchema = EvoluSchema>

/** Use this option to create initial data (fixtures). */
initialData: (evolu: EvoluForInitialData<T>) => void;

/**
* Use this option to create Evolu with the specified mnemonic. If omitted,
* the mnemonic will be autogenerated. That should be the default behavior
* until special UX requirements are needed (e.g., multitenancy).
*/
mnemonic: Mnemonic;
}

const schemaToTables = (schema: S.Schema<any>) =>
Expand Down Expand Up @@ -565,7 +597,8 @@ const getPropertySignatures = <I extends { [K in keyof A]: any }, A>(
const createEvolu = (
schema: DbSchema,
runtime: ManagedRuntime.ManagedRuntime<Config, never>,
initialData?: EvoluConfig["initialData"],
initialData: EvoluConfig["initialData"],
mnemonic: Mnemonic | undefined,
) =>
Effect.gen(function* () {
yield* Effect.logTrace("EvoluFactory createEvolu");
Expand Down Expand Up @@ -640,6 +673,7 @@ const createEvolu = (
handleDbError,
handleSyncStateChange,
handleDbReceive,
mnemonic,
).pipe(
Effect.tap(sync({ refreshQueries: false })),
Effect.flatMap(ownerStore.setState),
Expand Down Expand Up @@ -861,15 +895,20 @@ const createEvolu = (
update: mutate,
createOrUpdate: mutate as Mutate<EvoluSchema, "createOrUpdate">,

resetOwner: () => {
db.resetOwner().pipe(Effect.zipRight(appStateReset.reset), runFork);
},
resetOwner: (options) =>
Effect.gen(function* () {
yield* db.resetOwner();
if (options?.reload !== false) yield* appStateReset.reset;
}).pipe(runPromise),

restoreOwner: (mnemonic) => {
db.restoreOwner(schema, mnemonic).pipe(
Effect.zipRight(appStateReset.reset),
runFork,
);
restoreOwner: (mnemonic, options) =>
Effect.gen(function* () {
yield* db.restoreOwner(schema, mnemonic);
if (options?.reload !== false) yield* appStateReset.reset;
}).pipe(runPromise),

reloadApp: () => {
appStateReset.reset.pipe(runFork);
},

ensureSchema: (schema) => {
Expand Down

0 comments on commit e420fec

Please sign in to comment.