Skip to content

Commit

Permalink
Close #57
Browse files Browse the repository at this point in the history
  • Loading branch information
steida committed May 8, 2024
1 parent f0791db commit 79a6d0c
Show file tree
Hide file tree
Showing 12 changed files with 297 additions and 199 deletions.
25 changes: 25 additions & 0 deletions .changeset/nine-cameras-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
"@evolu/common": minor
---

Time Travel

Evolu does not delete data; it only marks them as deleted. This is because local-first is a distributed system. There is no central authority (if there is, it's not local-first). Imagine you delete data on some disconnected device and update it on another. Should we throw away changes? Such a deletion would require additional logic to enforce data deletion on all devices forever, even in the future, when some outdated device syncs. It's possible (and planned for Evolu), but it's not trivial because every device has to track data to be rejected without knowing the data itself (for security reasons).

Not deleting data allows Evolu to provide a time-traveling feature. All data, even "deleted" or overridden, are stored in the evolu_message table. Here is how we can read such data.

```ts
const todoTitleHistory = (id: TodoId) =>
evolu.createQuery((db) =>
db
.selectFrom("evolu_message")
.select("value")
.where("table", "==", "todo")
.where("row", "==", id)
.where("column", "==", "title")
.$narrowType<{ value: TodoTable["title"] }>()
.orderBy("timestamp", "desc"),
);
```

Note that this API is not 100% typed, but it's not an issue because Evolu Schema shall be append-only. Once an app is released, we shall not change Schema names and types. We can only add new tables and columns because there is a chance current Schema is already used.
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"devDependencies": {
"@evolu/tsconfig": "workspace:*",
"@types/node": "^20.12.8",
"@types/node": "^20.12.10",
"ts-node": "10.9.2",
"typescript": "^5.4.5"
},
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
},
"devDependencies": {
"@evolu/tsconfig": "workspace:*",
"@types/node": "^20.12.8",
"@types/node": "^20.12.10",
"@types/react": "~18.3.1",
"@types/react-dom": "~18.3.0",
"autoprefixer": "^10.4.19",
Expand Down
1 change: 1 addition & 0 deletions apps/web/pages/docs/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"quickstart": "Quickstart",
"indexes": "Indexes",
"migrations": "Migrations",
"time-travel": "Time Travel",
"patterns": "Patterns",
"evolu-server": "Evolu Server",
"how-evolu-works": "How Evolu Works",
Expand Down
21 changes: 21 additions & 0 deletions apps/web/pages/docs/time-travel.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Time Travel

Evolu does not delete data; it only marks them as deleted. This is because local-first is a distributed system. There is no central authority (if there is, it's not local-first). Imagine you delete data on some disconnected device and update it on another. Should we throw away changes? Such a deletion would require additional logic to enforce data deletion on all devices forever, even in the future, when some outdated device syncs. It's possible (and planned for Evolu), but it's not trivial because every device has to track data to be rejected without knowing the data itself (for security reasons).

Not deleting data allows Evolu to provide a time-traveling feature. All data, even "deleted" or overridden, are stored in the evolu_message table. Here is how we can read such data.

```ts
const todoTitleHistory = (id: TodoId) =>
evolu.createQuery((db) =>
db
.selectFrom("evolu_message")
.select("value")
.where("table", "==", "todo")
.where("row", "==", id)
.where("column", "==", "title")
.$narrowType<{ value: TodoTable["title"] }>()
.orderBy("timestamp", "desc"),
);
```

Note that this API is not 100% typed, but it's not an issue because Evolu Schema shall be append-only. Once an app is released, we shall not change Schema names and types. We can only add new tables and columns because there is a chance current Schema is already used.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
},
"packageManager": "[email protected]",
"pnpm": {
"patchedDependencies": {
"@changesets/[email protected]": "patches/@[email protected]"
},
"peerDependencyRules": {
"ignoreMissing": [
"@babel/*",
Expand Down
4 changes: 4 additions & 0 deletions packages/evolu-common/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Local-first apps allow users to own their data by storing them on their devices.

For detailed information and usage examples, please visit [evolu.dev](https://www.evolu.dev).

## API Reference

[evoluhq.github.io/evolu](https://evoluhq.github.io/evolu)

## Community

The Evolu community is on [GitHub Discussions](https://github.com/evoluhq/evolu/discussions), where you can ask questions and voice ideas.
Expand Down
10 changes: 5 additions & 5 deletions packages/evolu-common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@
"protobuf": "pnpm protoc --ts_out ./src --proto_path protobuf protobuf/Protobuf.proto --ts_opt eslint_disable --ts_opt optimize_code_size && pnpm format"
},
"dependencies": {
"@noble/ciphers": "^0.5.2",
"@noble/ciphers": "^0.5.3",
"@noble/hashes": "^1.4.0",
"@protobuf-ts/runtime": "^2.9.4",
"@scure/bip39": "^1.3.0",
"kysely": "^0.27.3",
"nanoid": "^5.0.7"
},
"devDependencies": {
"@effect/platform": "^0.52.2",
"@effect/platform": "^0.53.0",
"@effect/schema": "^0.66.14",
"@evolu/tsconfig": "workspace:*",
"@protobuf-ts/plugin": "^2.9.4",
Expand All @@ -74,9 +74,9 @@
"vitest": "^1.6.0"
},
"peerDependencies": {
"@effect/platform": "^0.52.2",
"@effect/schema": "^0.66.10",
"effect": "^3.0.0"
"@effect/platform": "^0.53.0",
"@effect/schema": "^0.66.14",
"effect": "^3.1.2"
},
"publishConfig": {
"access": "public"
Expand Down
23 changes: 17 additions & 6 deletions packages/evolu-common/src/Evolu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ 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 @@ -101,11 +102,21 @@ export interface Evolu<T extends EvoluSchema = EvoluSchema> {
readonly createQuery: <R extends Row>(
queryCallback: (
db: Pick<
Kysely.Kysely<{
[Table in keyof T]: NullableExceptIdCreatedAtUpdatedAt<{
[Column in keyof T[Table]]: T[Table][Column];
}>;
}>,
Kysely.Kysely<
{
[Table in keyof T]: NullableExceptIdCreatedAtUpdatedAt<{
[Column in keyof T[Table]]: T[Table][Column];
}>;
} & {
readonly evolu_message: {
readonly timestamp: TimestampString;
readonly table: keyof T;
readonly row: Id;
readonly column: string;
readonly value: Value;
};
}
>,
"selectFrom" | "fn" | "with" | "withRecursive"
>,
) => Kysely.SelectQueryBuilder<any, any, R>,
Expand Down Expand Up @@ -764,7 +775,7 @@ const createEvolu = (

createQuery: (queryCallback, options) =>
pipe(
queryCallback(kysely).compile(),
queryCallback(kysely as never).compile(),
(compiledQuery): SqliteQuery => {
if (isSqlMutation(compiledQuery.sql))
throw new Error(
Expand Down
2 changes: 1 addition & 1 deletion packages/evolu-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"@types/body-parser": "^1.19.5",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^20.12.8",
"@types/node": "^20.12.10",
"eslint": "^8.57.0",
"eslint-config-evolu": "workspace:*",
"typescript": "^5.4.5",
Expand Down
42 changes: 42 additions & 0 deletions patches/@[email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
diff --git a/dist/changesets-assemble-release-plan.cjs.js b/dist/changesets-assemble-release-plan.cjs.js
index ee5c0f67fabadeb112e9f238d8b144a4d125830f..42afcbce044a2949d6242af0a6c5abffcd2a51ad 100644
--- a/dist/changesets-assemble-release-plan.cjs.js
+++ b/dist/changesets-assemble-release-plan.cjs.js
@@ -90,16 +90,6 @@ function determineDependents({
} of dependencyVersionRanges) {
if (nextRelease.type === "none") {
continue;
- } else if (shouldBumpMajor({
- dependent,
- depType,
- versionRange,
- releases,
- nextRelease,
- preInfo,
- onlyUpdatePeerDependentsWhenOutOfRange: config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.onlyUpdatePeerDependentsWhenOutOfRange
- })) {
- type = "major";
} else if ((!releases.has(dependent) || releases.get(dependent).type === "none") && (config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.updateInternalDependents === "always" || !semverSatisfies__default["default"](incrementVersion(nextRelease, preInfo), versionRange))) {
switch (depType) {
case "dependencies":
diff --git a/dist/changesets-assemble-release-plan.esm.js b/dist/changesets-assemble-release-plan.esm.js
index bf5202626a164a7780650d333983c3479b078689..27eea4d1d31c0e7ce8d56363c5a3437bbeb819a0 100644
--- a/dist/changesets-assemble-release-plan.esm.js
+++ b/dist/changesets-assemble-release-plan.esm.js
@@ -79,16 +79,6 @@ function determineDependents({
} of dependencyVersionRanges) {
if (nextRelease.type === "none") {
continue;
- } else if (shouldBumpMajor({
- dependent,
- depType,
- versionRange,
- releases,
- nextRelease,
- preInfo,
- onlyUpdatePeerDependentsWhenOutOfRange: config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.onlyUpdatePeerDependentsWhenOutOfRange
- })) {
- type = "major";
} else if ((!releases.has(dependent) || releases.get(dependent).type === "none") && (config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.updateInternalDependents === "always" || !semverSatisfies(incrementVersion(nextRelease, preInfo), versionRange))) {
switch (depType) {
case "dependencies":
Loading

0 comments on commit 79a6d0c

Please sign in to comment.