@evolu/common 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/src/Config.d.ts +20 -0
- package/dist/src/Config.d.ts.map +1 -0
- package/dist/src/Config.js +8 -0
- package/dist/src/Crypto.d.ts +42 -0
- package/dist/src/Crypto.d.ts.map +1 -0
- package/dist/src/Crypto.js +44 -0
- package/dist/src/Db.d.ts +60 -0
- package/dist/src/Db.d.ts.map +1 -0
- package/dist/src/Db.js +118 -0
- package/dist/src/DbWorker.d.ts +78 -0
- package/dist/src/DbWorker.d.ts.map +1 -0
- package/dist/src/DbWorker.js +282 -0
- package/dist/src/Diff.d.ts +19 -0
- package/dist/src/Diff.d.ts.map +1 -0
- package/dist/src/Diff.js +86 -0
- package/dist/src/Errors.d.ts +19 -0
- package/dist/src/Errors.d.ts.map +1 -0
- package/dist/src/Errors.js +11 -0
- package/dist/src/Evolu.d.ts +71 -0
- package/dist/src/Evolu.d.ts.map +1 -0
- package/dist/src/Evolu.js +235 -0
- package/dist/src/MerkleTree.d.ts +15 -0
- package/dist/src/MerkleTree.d.ts.map +1 -0
- package/dist/src/MerkleTree.js +49 -0
- package/dist/src/Model.d.ts +143 -0
- package/dist/src/Model.d.ts.map +1 -0
- package/dist/src/Model.js +103 -0
- package/dist/src/Murmurhash.d.ts +2 -0
- package/dist/src/Murmurhash.d.ts.map +1 -0
- package/dist/src/Murmurhash.js +60 -0
- package/dist/src/Platform.d.ts +29 -0
- package/dist/src/Platform.d.ts.map +1 -0
- package/dist/src/Platform.js +17 -0
- package/dist/src/Protobuf.d.ts +130 -0
- package/dist/src/Protobuf.d.ts.map +1 -0
- package/dist/src/Protobuf.js +100 -0
- package/dist/src/Public.d.ts +7 -0
- package/dist/src/Public.d.ts.map +1 -0
- package/dist/src/Public.js +2 -0
- package/dist/src/Sql.d.ts +12 -0
- package/dist/src/Sql.d.ts.map +1 -0
- package/dist/src/Sql.js +100 -0
- package/dist/src/Sqlite.d.ts +30 -0
- package/dist/src/Sqlite.d.ts.map +1 -0
- package/dist/src/Sqlite.js +41 -0
- package/dist/src/Store.d.ts +9 -0
- package/dist/src/Store.d.ts.map +1 -0
- package/dist/src/Store.js +18 -0
- package/dist/src/SyncWorker.d.ts +73 -0
- package/dist/src/SyncWorker.d.ts.map +1 -0
- package/dist/src/SyncWorker.js +136 -0
- package/dist/src/Timestamp.d.ts +44 -0
- package/dist/src/Timestamp.d.ts.map +1 -0
- package/dist/src/Timestamp.js +76 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +18 -0
- package/package.json +76 -0
- package/src/Config.ts +33 -0
- package/src/Crypto.ts +113 -0
- package/src/Db.ts +323 -0
- package/src/DbWorker.ts +683 -0
- package/src/Diff.ts +113 -0
- package/src/Errors.ts +33 -0
- package/src/Evolu.ts +527 -0
- package/src/MerkleTree.ts +83 -0
- package/src/Model.ts +212 -0
- package/src/Murmurhash.ts +70 -0
- package/src/Platform.ts +63 -0
- package/src/Protobuf.ts +204 -0
- package/src/Public.ts +6 -0
- package/src/Sql.ts +114 -0
- package/src/Sqlite.ts +92 -0
- package/src/Store.ts +32 -0
- package/src/SyncWorker.ts +344 -0
- package/src/Timestamp.ts +192 -0
- package/src/index.ts +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Evolu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Evolu
|
|
2
|
+
|
|
3
|
+
[Local-first](https://www.inkandswitch.com/local-first/) platform designed for privacy, ease of use, and no vendor lock-in to sync&back precious data.
|
|
4
|
+
|
|
5
|
+
- [SQLite](https://sqlite.org/) in all browsers, Electron, and React Native
|
|
6
|
+
- E2E encrypted sync and backup with [CRDT](https://crdt.tech/) (merging changes without conflicts)
|
|
7
|
+
- Free Evolu server for testing (paid production-ready soon, or you can run your own)
|
|
8
|
+
- Typed database schema with branded types (`NonEmptyString1000`, `PositiveInt`, etc.)
|
|
9
|
+
- Reactive queries with React Suspense support
|
|
10
|
+
- Real-time experience via revalidation on focus and network recovery
|
|
11
|
+
- Schema evolving via `filterMap` ad-hoc migration
|
|
12
|
+
- No signup/login, no email collection, only Bitcoin-like mnemonic (12 words)
|
|
13
|
+
- Fetching nested objects and arrays in a single SQL query
|
|
14
|
+
- JSON support with automatic parsing and stringifying
|
|
15
|
+
|
|
16
|
+
## Local-first apps
|
|
17
|
+
|
|
18
|
+
Local-first apps allow users to own their data. Evolu stores data in the user's device(s), so Evolu apps can work offline and without a specific server. How is it different from keeping files on disk? Files are not the right abstraction for apps and are complicated to synchronize among devices. That's why client-server architecture rules the world. But as with everything, it has trade-offs.
|
|
19
|
+
|
|
20
|
+
### The trade-offs of the client-server architecture
|
|
21
|
+
|
|
22
|
+
Client-server architecture provides us with easy backup and synchronization, but all that depends on the ability of a server to fulfill its promises. Internet is offline, companies go bankrupt, users are banned, and errors occur. All those things happen all the time, and then what? Right, that's why the world needs local-first apps. But until now, writing local-first apps has been challenging because of the lack of libraries and design patterns. That's why I created Evolu.
|
|
23
|
+
|
|
24
|
+
## Overview
|
|
25
|
+
|
|
26
|
+
### Define Data
|
|
27
|
+
|
|
28
|
+
To start using Evolu, define tables for your database and export React Hooks.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import * as Schema from "@effect/schema/Schema";
|
|
32
|
+
import * as Evolu from "@evolu/react";
|
|
33
|
+
|
|
34
|
+
const TodoId = Evolu.id("Todo");
|
|
35
|
+
type TodoId = Schema.Schema.To<typeof TodoId>;
|
|
36
|
+
|
|
37
|
+
const TodoTable = Schema.struct({
|
|
38
|
+
id: TodoId,
|
|
39
|
+
title: Evolu.NonEmptyString1000,
|
|
40
|
+
isCompleted: Evolu.SqliteBoolean,
|
|
41
|
+
});
|
|
42
|
+
type TodoTable = Schema.Schema.To<typeof TodoTable>;
|
|
43
|
+
|
|
44
|
+
const Database = Schema.struct({
|
|
45
|
+
todo: TodoTable,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const {
|
|
49
|
+
useQuery,
|
|
50
|
+
useMutation,
|
|
51
|
+
useOwner,
|
|
52
|
+
useOwnerActions,
|
|
53
|
+
useEvoluError,
|
|
54
|
+
} = Evolu.create(Database);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Validate Data
|
|
58
|
+
|
|
59
|
+
Learn more about [Schema](https://github.com/effect-ts/schema).
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import * as Schema from "@effect/schema/Schema";
|
|
63
|
+
import * as Evolu from "@evolu/react";
|
|
64
|
+
|
|
65
|
+
Schema.parse(Evolu.String1000)(title);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Mutate Data
|
|
69
|
+
|
|
70
|
+
Mutation API is designed for local-first apps to ensure changes are always merged without conflicts.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const { create, update } = useMutation();
|
|
74
|
+
|
|
75
|
+
create("todo", { title, isCompleted: false });
|
|
76
|
+
update("todo", { id, isCompleted: true });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Query Data
|
|
80
|
+
|
|
81
|
+
Evolu uses type-safe TypeScript SQL query builder [kysely](https://github.com/koskimas/kysely), so autocompletion works out-of-the-box.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const { rows } = useQuery(
|
|
85
|
+
(db) => db.selectFrom("todo").select(["id", "title"]).orderBy("updatedAt"),
|
|
86
|
+
// (row) => row
|
|
87
|
+
({ title, ...rest }) => title && { title, ...rest },
|
|
88
|
+
);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Protect Data
|
|
92
|
+
|
|
93
|
+
Evolu encrypts data with Mnemonic, a safe autogenerated password based on [bip39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki).
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const owner = useOwner();
|
|
97
|
+
|
|
98
|
+
alert(owner.mnemonic);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Delete Data
|
|
102
|
+
|
|
103
|
+
Leave no traces on a device.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const ownerActions = useOwnerActions();
|
|
107
|
+
|
|
108
|
+
if (confirm("Are you sure? It will delete all your local data."))
|
|
109
|
+
ownerActions.reset();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Restore Data
|
|
113
|
+
|
|
114
|
+
Restore data elsewhere. Encrypted data can only be restored with a Mnemonic.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const ownerActions = useOwnerActions();
|
|
118
|
+
|
|
119
|
+
ownerActions.restore(mnemonic).then((either) => {
|
|
120
|
+
if (either._tag === "Left") alert(JSON.stringify(either.left, null, 2));
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Handle Errors
|
|
125
|
+
|
|
126
|
+
Evolu `useQuery` and `useMutation` never fail, it's the advantage of local first apps, but Evolu, in rare cases, can.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const evoluError = useEvoluError();
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
// eslint-disable-next-line no-console
|
|
133
|
+
if (evoluError) console.log(evoluError);
|
|
134
|
+
}, [evoluError]);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
And that's all. Minimal API is the key to a great developer experience.
|
|
138
|
+
|
|
139
|
+
## Privacy
|
|
140
|
+
|
|
141
|
+
Evolu uses end-to-end encryption and generates strong and safe passwords for you. Evolu sync and backup server see only userId and timestamps.
|
|
142
|
+
|
|
143
|
+
## Trade-offs
|
|
144
|
+
|
|
145
|
+
> “There are no solutions. There are only trade-offs.” ― Thomas Sowell
|
|
146
|
+
|
|
147
|
+
Evolu is not P2P. For reliable syncing and backup, there needs to be a server. Evolu server is very minimal, and everyone can run their own. While it's theoretically possible to have P2P Evolu, I have yet to see a reliable solution. It's not only a technical problem; it's an economic problem. Someone has to be paid to keep your data safe. Evolu provides a free server for testing. Soon we will provide a paid server for production usage.
|
|
148
|
+
|
|
149
|
+
All table columns except for ID are nullable by default. It's not a bug; it's a feature. Local-first data are meant to last forever, but schemas evolve. This design decision was inspired by GraphQL [nullability](https://graphql.org/learn/best-practices/#nullability) and [versioning](https://graphql.org/learn/best-practices/#versioning). Evolu provides a handy `filterMap` helper for queries.
|
|
150
|
+
|
|
151
|
+
## Community
|
|
152
|
+
|
|
153
|
+
The Evolu community is on GitHub Discussions, where you can ask questions and voice ideas.
|
|
154
|
+
|
|
155
|
+
To chat with other community members, you can join the [Evolu Discord](https://discord.gg/2J8yyyyxtZ).
|
|
156
|
+
|
|
157
|
+
[](https://twitter.com/evoluhq)
|
|
158
|
+
|
|
159
|
+
## FAQ
|
|
160
|
+
|
|
161
|
+
### Is Evolu ready for production?
|
|
162
|
+
|
|
163
|
+
Yes. Evolu is stable.
|
|
164
|
+
|
|
165
|
+
### What is the SQLite database size limit?
|
|
166
|
+
|
|
167
|
+
[Storage quotas and eviction criteria](https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria)
|
|
168
|
+
|
|
169
|
+
### How can I check the current database filesize?
|
|
170
|
+
|
|
171
|
+
Use OPFS Explorer Chrome DevTools extension.
|
|
172
|
+
|
|
173
|
+
## Contributing
|
|
174
|
+
|
|
175
|
+
Evolu monorepo uses [pnpm](https://pnpm.io).
|
|
176
|
+
|
|
177
|
+
Install the dependencies with:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
pnpm install
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Build Evolu monorepo:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
pnpm build
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Start developing and watch for code changes:
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
pnpm dev
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Lint and tests:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
pnpm lint test
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Describe changes for release log:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
pnpm changeset
|
|
205
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Context, Layer } from "effect";
|
|
2
|
+
export interface Config {
|
|
3
|
+
/**
|
|
4
|
+
* Alternate URL to Evolu sync&backup server.
|
|
5
|
+
*/
|
|
6
|
+
readonly syncUrl: string;
|
|
7
|
+
/**
|
|
8
|
+
* Alternate URL to reload browser tabs after `Owner` reset or restore.
|
|
9
|
+
* The default value is `/`.
|
|
10
|
+
*/
|
|
11
|
+
readonly reloadUrl: string;
|
|
12
|
+
/**
|
|
13
|
+
* Maximum physical clock drift allowed in ms.
|
|
14
|
+
* The default value is 5 * 60 * 1000 (5 minutes).
|
|
15
|
+
*/
|
|
16
|
+
readonly maxDrift: number;
|
|
17
|
+
}
|
|
18
|
+
export declare const Config: Context.Tag<Config, Config>;
|
|
19
|
+
export declare const ConfigLive: (config?: Partial<Config>) => Layer.Layer<never, never, Config>;
|
|
20
|
+
//# sourceMappingURL=Config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Config.d.ts","sourceRoot":"","sources":["../../src/Config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAExC,MAAM,WAAW,MAAM;IACrB;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,MAAM,6BAAsC,CAAC;AAE1D,eAAO,MAAM,UAAU,YACZ,QAAQ,MAAM,CAAC,KACvB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAShC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Context, Layer } from "effect";
|
|
2
|
+
export const Config = Context.Tag("evolu/Config");
|
|
3
|
+
export const ConfigLive = (config) => Layer.succeed(Config, Config.of({
|
|
4
|
+
syncUrl: "https://evolu.world",
|
|
5
|
+
maxDrift: 5 * 60 * 1000,
|
|
6
|
+
reloadUrl: "/",
|
|
7
|
+
...config,
|
|
8
|
+
}));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as Schema from "@effect/schema/Schema";
|
|
2
|
+
import { Brand, Context, Effect, Layer } from "effect";
|
|
3
|
+
export interface Bip39 {
|
|
4
|
+
readonly make: Effect.Effect<never, never, Mnemonic>;
|
|
5
|
+
readonly toSeed: (mnemonic: Mnemonic) => Effect.Effect<never, never, Uint8Array>;
|
|
6
|
+
readonly parse: (mnemonic: string) => Effect.Effect<never, InvalidMnemonicError, Mnemonic>;
|
|
7
|
+
}
|
|
8
|
+
export declare const Bip39: Context.Tag<Bip39, Bip39>;
|
|
9
|
+
export interface InvalidMnemonicError {
|
|
10
|
+
readonly _tag: "InvalidMnemonicError";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Mnemonic is a password generated by Evolu in BIP39 format.
|
|
14
|
+
*
|
|
15
|
+
* A mnemonic, also known as a "seed phrase," is a set of 12 words in a
|
|
16
|
+
* specific order chosen from a predefined list. The purpose of the BIP39
|
|
17
|
+
* mnemonic is to provide a human-readable way of storing a private key.
|
|
18
|
+
*/
|
|
19
|
+
export type Mnemonic = string & Brand.Brand<"Mnemonic">;
|
|
20
|
+
export interface NanoId {
|
|
21
|
+
readonly nanoid: Effect.Effect<never, never, string>;
|
|
22
|
+
readonly nanoidAsNodeId: Effect.Effect<never, never, NodeId>;
|
|
23
|
+
}
|
|
24
|
+
export declare const NanoId: Context.Tag<NanoId, NanoId>;
|
|
25
|
+
export declare const NodeId: Schema.BrandSchema<string, string & Brand.Brand<"NodeId">>;
|
|
26
|
+
export type NodeId = Schema.Schema.To<typeof NodeId>;
|
|
27
|
+
export declare const NanoIdLive: Layer.Layer<never, never, NanoId>;
|
|
28
|
+
/**
|
|
29
|
+
* SLIP-21 implementation
|
|
30
|
+
* https://github.com/satoshilabs/slips/blob/master/slip-0021.md
|
|
31
|
+
*/
|
|
32
|
+
export declare const slip21Derive: (seed: Uint8Array, path: string[]) => Effect.Effect<never, never, Uint8Array>;
|
|
33
|
+
/**
|
|
34
|
+
* Alias to xsalsa20poly1305, for compatibility with libsodium / nacl
|
|
35
|
+
*/
|
|
36
|
+
export interface SecretBox {
|
|
37
|
+
readonly seal: (key: Uint8Array, plaintext: Uint8Array) => Effect.Effect<never, never, Uint8Array>;
|
|
38
|
+
readonly open: (key: Uint8Array, ciphertext: Uint8Array) => Effect.Effect<never, never, Uint8Array>;
|
|
39
|
+
}
|
|
40
|
+
export declare const SecretBox: Context.Tag<SecretBox, SecretBox>;
|
|
41
|
+
export declare const SecretBoxLive: Layer.Layer<never, never, SecretBox>;
|
|
42
|
+
//# sourceMappingURL=Crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Crypto.d.ts","sourceRoot":"","sources":["../../src/Crypto.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,uBAAuB,CAAC;AAMhD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAGvD,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;IAErD,QAAQ,CAAC,MAAM,EAAE,CACf,QAAQ,EAAE,QAAQ,KACf,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC;IAE7C,QAAQ,CAAC,KAAK,EAAE,CACd,QAAQ,EAAE,MAAM,KACb,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,oBAAoB,EAAE,QAAQ,CAAC,CAAC;CAC3D;AAED,eAAO,MAAM,KAAK,2BAAoC,CAAC;AAEvD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;CACvC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;AAExD,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IACrD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;CAC9D;AAED,eAAO,MAAM,MAAM,6BAAsC,CAAC;AAE1D,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,WAAW,CACrC,MAAM,EACN,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAC6C,CAAC;AAC9E,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,MAAM,CAAC,CAAC;AAIrD,eAAO,MAAM,UAAU,mCAMtB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,YAAY,SACjB,UAAU,QACV,MAAM,EAAE,KACb,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,CAWrC,CAAC;AAEL;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,CACb,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,UAAU,KAClB,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC;IAE7C,QAAQ,CAAC,IAAI,EAAE,CACb,GAAG,EAAE,UAAU,EACf,UAAU,EAAE,UAAU,KACnB,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC;CAC9C;AAED,eAAO,MAAM,SAAS,mCAA4C,CAAC;AAEnE,eAAO,MAAM,aAAa,sCAgBzB,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as Schema from "@effect/schema/Schema";
|
|
2
|
+
import { secretbox } from "@noble/ciphers/salsa";
|
|
3
|
+
import { concatBytes } from "@noble/ciphers/utils";
|
|
4
|
+
import { hmac } from "@noble/hashes/hmac";
|
|
5
|
+
import { sha512 } from "@noble/hashes/sha512";
|
|
6
|
+
import { randomBytes } from "@noble/hashes/utils";
|
|
7
|
+
import { Context, Effect, Layer } from "effect";
|
|
8
|
+
import { customAlphabet, nanoid } from "nanoid";
|
|
9
|
+
export const Bip39 = Context.Tag("evolu/Bip39");
|
|
10
|
+
export const NanoId = Context.Tag("evolu/NanoId");
|
|
11
|
+
export const NodeId = Schema.string.pipe(Schema.pattern(/^[\w-]{16}$/), Schema.brand("NodeId"));
|
|
12
|
+
const nanoidForNodeId = customAlphabet("0123456789abcdef", 16);
|
|
13
|
+
export const NanoIdLive = Layer.succeed(NanoId, NanoId.of({
|
|
14
|
+
nanoid: Effect.sync(() => nanoid()),
|
|
15
|
+
nanoidAsNodeId: Effect.sync(() => nanoidForNodeId()),
|
|
16
|
+
}));
|
|
17
|
+
/**
|
|
18
|
+
* SLIP-21 implementation
|
|
19
|
+
* https://github.com/satoshilabs/slips/blob/master/slip-0021.md
|
|
20
|
+
*/
|
|
21
|
+
export const slip21Derive = (seed, path) => Effect.sync(() => {
|
|
22
|
+
let m = hmac(sha512, "Symmetric key seed", seed);
|
|
23
|
+
for (let i = 0; i < path.length; i++) {
|
|
24
|
+
const p = new TextEncoder().encode(path[i]);
|
|
25
|
+
const e = new Uint8Array(p.byteLength + 1);
|
|
26
|
+
e[0] = 0;
|
|
27
|
+
e.set(p, 1);
|
|
28
|
+
m = hmac(sha512, m.slice(0, 32), e);
|
|
29
|
+
}
|
|
30
|
+
return m.slice(32, 64);
|
|
31
|
+
});
|
|
32
|
+
export const SecretBox = Context.Tag("evolu/SecretBox");
|
|
33
|
+
export const SecretBoxLive = Layer.succeed(SecretBox, SecretBox.of({
|
|
34
|
+
seal: (key, plaintext) => Effect.sync(() => {
|
|
35
|
+
const nonce = randomBytes(24);
|
|
36
|
+
const ciphertext = secretbox(key, nonce).seal(plaintext);
|
|
37
|
+
return concatBytes(nonce, ciphertext);
|
|
38
|
+
}),
|
|
39
|
+
open: (key, ciphertext) => Effect.sync(() => {
|
|
40
|
+
const nonce = ciphertext.subarray(0, 24);
|
|
41
|
+
const ciphertextWithoutNonce = ciphertext.subarray(24);
|
|
42
|
+
return secretbox(key, nonce).open(ciphertextWithoutNonce);
|
|
43
|
+
}),
|
|
44
|
+
}));
|
package/dist/src/Db.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as S from "@effect/schema/Schema";
|
|
2
|
+
import { Brand, Context, Effect, ReadonlyRecord } from "effect";
|
|
3
|
+
import * as Kysely from "kysely";
|
|
4
|
+
import { Bip39, Mnemonic, NanoId } from "./Crypto.js";
|
|
5
|
+
import { Id, SqliteBoolean, SqliteDate } from "./Model.js";
|
|
6
|
+
import { Query, Row, Sqlite, Value } from "./Sqlite.js";
|
|
7
|
+
export type TableSchema = ReadonlyRecord.ReadonlyRecord<Value> & {
|
|
8
|
+
readonly id: Id;
|
|
9
|
+
};
|
|
10
|
+
export type Schema = ReadonlyRecord.ReadonlyRecord<TableSchema>;
|
|
11
|
+
export type CreateQuery<S extends Schema> = (queryCallback: QueryCallback<S, Row>) => Query;
|
|
12
|
+
export type QueryCallback<S extends Schema, QueryRow> = (db: KyselyWithoutMutation<SchemaForQuery<S>>) => Kysely.SelectQueryBuilder<any, any, QueryRow>;
|
|
13
|
+
type KyselyWithoutMutation<DB> = Pick<Kysely.Kysely<DB>, "selectFrom" | "fn">;
|
|
14
|
+
type SchemaForQuery<S extends Schema> = {
|
|
15
|
+
readonly [Table in keyof S]: NullableExceptOfId<{
|
|
16
|
+
readonly [Column in keyof S[Table]]: S[Table][Column];
|
|
17
|
+
} & CommonColumns>;
|
|
18
|
+
};
|
|
19
|
+
export type NullableExceptOfId<T> = {
|
|
20
|
+
readonly [K in keyof T]: K extends "id" ? T[K] : T[K] | null;
|
|
21
|
+
};
|
|
22
|
+
export interface CommonColumns {
|
|
23
|
+
readonly createdAt: SqliteDate;
|
|
24
|
+
readonly updatedAt: SqliteDate;
|
|
25
|
+
readonly isDeleted: SqliteBoolean;
|
|
26
|
+
}
|
|
27
|
+
export interface Table {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly columns: ReadonlyArray<string>;
|
|
30
|
+
}
|
|
31
|
+
export type Tables = ReadonlyArray<Table>;
|
|
32
|
+
export declare const makeCreateQuery: <T extends Schema>() => CreateQuery<T>;
|
|
33
|
+
export declare const schemaToTables: (schema: S.Schema<any, any>) => Tables;
|
|
34
|
+
/**
|
|
35
|
+
* `Owner` represents the Evolu database owner. Evolu auto-generates `Owner`
|
|
36
|
+
* on the first run. `Owner` can be reset on the current device and restored
|
|
37
|
+
* on a different one.
|
|
38
|
+
*/
|
|
39
|
+
export interface Owner {
|
|
40
|
+
/** The `Mnemonic` associated with `Owner`. */
|
|
41
|
+
readonly mnemonic: Mnemonic;
|
|
42
|
+
/** The unique identifier of `Owner` safely derived from its `Mnemonic`. */
|
|
43
|
+
readonly id: OwnerId;
|
|
44
|
+
readonly encryptionKey: Uint8Array;
|
|
45
|
+
}
|
|
46
|
+
export declare const Owner: Context.Tag<Owner, Owner>;
|
|
47
|
+
/**
|
|
48
|
+
* The unique identifier of `Owner` safely derived from its `Mnemonic`.
|
|
49
|
+
*/
|
|
50
|
+
export type OwnerId = Id & Brand.Brand<"Owner">;
|
|
51
|
+
export declare const transaction: <R, E, A>(effect: Effect.Effect<R, E, A>) => Effect.Effect<Sqlite | R, E, A>;
|
|
52
|
+
export interface NoSuchTableOrColumnError {
|
|
53
|
+
readonly _tag: "NoSuchTableOrColumnError";
|
|
54
|
+
}
|
|
55
|
+
export declare const someDefectToNoSuchTableOrColumnError: <R, E, A>(self: Effect.Effect<R, E, A>) => Effect.Effect<R, NoSuchTableOrColumnError | E, A>;
|
|
56
|
+
export declare const makeOwner: (mnemonic?: Mnemonic) => Effect.Effect<Bip39, never, Owner>;
|
|
57
|
+
export declare const lazyInit: (mnemonic?: Mnemonic) => Effect.Effect<Sqlite | Bip39 | NanoId, never, Owner>;
|
|
58
|
+
export declare const ensureSchema: (tables: Tables) => Effect.Effect<Sqlite, never, void>;
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=Db.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Db.d.ts","sourceRoot":"","sources":["../../src/Db.ts"],"names":[],"mappings":"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"}
|
package/dist/src/Db.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import * as AST from "@effect/schema/AST";
|
|
2
|
+
import { make } from "@effect/schema/Schema";
|
|
3
|
+
import { bytesToHex } from "@noble/ciphers/utils";
|
|
4
|
+
import { Context, Effect, Exit, Option, Predicate, ReadonlyArray, ReadonlyRecord, String, pipe, } from "effect";
|
|
5
|
+
import * as Kysely from "kysely";
|
|
6
|
+
import { urlAlphabet } from "nanoid";
|
|
7
|
+
import { Bip39, slip21Derive } from "./Crypto.js";
|
|
8
|
+
import { initialMerkleTree, merkleTreeToString } from "./MerkleTree.js";
|
|
9
|
+
import { createMessageTable, createMessageTableIndex, createOwnerTable, insertOwner, } from "./Sql.js";
|
|
10
|
+
import { Sqlite, queryObjectToQuery, } from "./Sqlite.js";
|
|
11
|
+
import { makeInitialTimestamp, timestampToString } from "./Timestamp.js";
|
|
12
|
+
const commonColumns = ["createdAt", "updatedAt", "isDeleted"];
|
|
13
|
+
const kysely = new Kysely.Kysely({
|
|
14
|
+
dialect: {
|
|
15
|
+
createAdapter: () => new Kysely.SqliteAdapter(),
|
|
16
|
+
createDriver: () => new Kysely.DummyDriver(),
|
|
17
|
+
createIntrospector() {
|
|
18
|
+
throw "Not implemeneted";
|
|
19
|
+
},
|
|
20
|
+
createQueryCompiler: () => new Kysely.SqliteQueryCompiler(),
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
export const makeCreateQuery = () => (queryCallback) => queryObjectToQuery(queryCallback(kysely).compile());
|
|
24
|
+
// https://github.com/Effect-TS/schema/releases/tag/v0.18.0
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
const getPropertySignatures = (schema) => {
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
const out = {};
|
|
29
|
+
const propertySignatures = AST.getPropertySignatures(schema.ast);
|
|
30
|
+
for (let i = 0; i < propertySignatures.length; i++) {
|
|
31
|
+
const propertySignature = propertySignatures[i];
|
|
32
|
+
out[propertySignature.name] = make(propertySignature.type);
|
|
33
|
+
}
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
|
|
35
|
+
return out;
|
|
36
|
+
};
|
|
37
|
+
export const schemaToTables = (
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
schema) => pipe(getPropertySignatures(schema), ReadonlyRecord.toEntries, ReadonlyArray.map(([name, schema]) => ({
|
|
40
|
+
name,
|
|
41
|
+
columns: Object.keys(getPropertySignatures(schema)).concat(commonColumns),
|
|
42
|
+
})));
|
|
43
|
+
export const Owner = Context.Tag("evolu/Owner");
|
|
44
|
+
export const transaction = (effect) => Effect.flatMap(Sqlite, (sqlite) => Effect.acquireUseRelease(sqlite.exec("BEGIN"), () => effect, (_, exit) => Exit.isFailure(exit) ? sqlite.exec("ROLLBACK") : sqlite.exec("END")));
|
|
45
|
+
export const someDefectToNoSuchTableOrColumnError = Effect.catchSomeDefect((error) => {
|
|
46
|
+
if (typeof error === "object" &&
|
|
47
|
+
error != null &&
|
|
48
|
+
"message" in error &&
|
|
49
|
+
typeof error.message === "string" &&
|
|
50
|
+
error.message.includes("code 1") &&
|
|
51
|
+
(error.message.includes("no such table") ||
|
|
52
|
+
error.message.includes("no such column") ||
|
|
53
|
+
error.message.includes("has no column")))
|
|
54
|
+
return Option.some(Effect.fail({
|
|
55
|
+
_tag: "NoSuchTableOrColumnError",
|
|
56
|
+
}));
|
|
57
|
+
return Option.none();
|
|
58
|
+
});
|
|
59
|
+
export const makeOwner = (mnemonic) => Effect.gen(function* (_) {
|
|
60
|
+
const bip39 = yield* _(Bip39);
|
|
61
|
+
if (mnemonic == null)
|
|
62
|
+
mnemonic = yield* _(bip39.make);
|
|
63
|
+
const seed = yield* _(bip39.toSeed(mnemonic));
|
|
64
|
+
const id = yield* _(slip21Derive(seed, ["Evolu", "Owner Id"]).pipe(Effect.map((key) => {
|
|
65
|
+
// convert key to nanoid
|
|
66
|
+
let id = "";
|
|
67
|
+
for (let i = 0; i < 21; i++) {
|
|
68
|
+
id += urlAlphabet[key[i] & 63];
|
|
69
|
+
}
|
|
70
|
+
return id;
|
|
71
|
+
})));
|
|
72
|
+
const encryptionKey = yield* _(slip21Derive(seed, ["Evolu", "Encryption Key"]));
|
|
73
|
+
return { mnemonic, id, encryptionKey };
|
|
74
|
+
});
|
|
75
|
+
export const lazyInit = (mnemonic) => Effect.gen(function* (_) {
|
|
76
|
+
const [owner, sqlite, initialTimestampString] = yield* _(Effect.all([makeOwner(mnemonic), Sqlite, makeInitialTimestamp], {
|
|
77
|
+
concurrency: "unbounded",
|
|
78
|
+
}));
|
|
79
|
+
yield* _(Effect.all([
|
|
80
|
+
sqlite.exec(createMessageTable),
|
|
81
|
+
sqlite.exec(createMessageTableIndex),
|
|
82
|
+
sqlite.exec(createOwnerTable),
|
|
83
|
+
sqlite.exec({
|
|
84
|
+
sql: insertOwner,
|
|
85
|
+
parameters: [
|
|
86
|
+
owner.id,
|
|
87
|
+
owner.mnemonic,
|
|
88
|
+
// expo-sqlite 11.3.2 doesn't support Uint8Array
|
|
89
|
+
bytesToHex(owner.encryptionKey),
|
|
90
|
+
timestampToString(initialTimestampString),
|
|
91
|
+
merkleTreeToString(initialMerkleTree),
|
|
92
|
+
],
|
|
93
|
+
}),
|
|
94
|
+
]));
|
|
95
|
+
return owner;
|
|
96
|
+
});
|
|
97
|
+
const getTables = Sqlite.pipe(Effect.flatMap((sqlite) => sqlite.exec(`SELECT "name" FROM "sqlite_schema" WHERE "type" = 'table'`)), Effect.map((result) => result.rows), Effect.map(ReadonlyArray.map((row) => row.name + "")), Effect.map(ReadonlyArray.filter(Predicate.not(String.startsWith("__")))), Effect.map(ReadonlyArray.dedupeWith(String.Equivalence)));
|
|
98
|
+
const updateTable = ({ name, columns, }) => Effect.gen(function* (_) {
|
|
99
|
+
const sqlite = yield* _(Sqlite);
|
|
100
|
+
const sql = yield* _(sqlite.exec(`PRAGMA table_info (${name})`), Effect.map((result) => result.rows), Effect.map(ReadonlyArray.map((row) => row.name)), Effect.map((existingColumns) => ReadonlyArray.differenceWith(String.Equivalence)(existingColumns)(columns)), Effect.map(ReadonlyArray.map((newColumn) => `ALTER TABLE "${name}" ADD COLUMN "${newColumn}" blob;`)), Effect.map(ReadonlyArray.join("")));
|
|
101
|
+
if (sql)
|
|
102
|
+
yield* _(sqlite.exec(sql));
|
|
103
|
+
});
|
|
104
|
+
const createTable = ({ name, columns, }) => Effect.flatMap(Sqlite, (sqlite) => sqlite.exec(`
|
|
105
|
+
CREATE TABLE ${name} (
|
|
106
|
+
"id" text primary key,
|
|
107
|
+
${columns
|
|
108
|
+
.filter((c) => c !== "id")
|
|
109
|
+
// "A column with affinity BLOB does not prefer one storage class over another
|
|
110
|
+
// and no attempt is made to coerce data from one storage class into another."
|
|
111
|
+
// https://www.sqlite.org/datatype3.html
|
|
112
|
+
.map((name) => `"${name}" blob`)
|
|
113
|
+
.join(", ")}
|
|
114
|
+
);
|
|
115
|
+
`));
|
|
116
|
+
export const ensureSchema = (tables) => Effect.flatMap(getTables, (existingTables) => Effect.forEach(tables, (tableDefinition) => existingTables.includes(tableDefinition.name)
|
|
117
|
+
? updateTable(tableDefinition)
|
|
118
|
+
: createTable(tableDefinition), { discard: true }));
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Brand, Context, Layer, ReadonlyArray, ReadonlyRecord } from "effect";
|
|
2
|
+
import { Config } from "./Config.js";
|
|
3
|
+
import { Bip39, Mnemonic, NanoId } from "./Crypto.js";
|
|
4
|
+
import { Owner, Tables } from "./Db.js";
|
|
5
|
+
import { QueryPatches } from "./Diff.js";
|
|
6
|
+
import { EvoluError } from "./Errors.js";
|
|
7
|
+
import { Id, SqliteDate } from "./Model.js";
|
|
8
|
+
import { Query, Row, Sqlite, Value } from "./Sqlite.js";
|
|
9
|
+
import { SyncState, SyncWorker, SyncWorkerOutputSyncResponse } from "./SyncWorker.js";
|
|
10
|
+
export interface DbWorker {
|
|
11
|
+
readonly postMessage: (input: DbWorkerInput) => void;
|
|
12
|
+
onMessage: (output: DbWorkerOutput) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare const DbWorker: Context.Tag<DbWorker, DbWorker>;
|
|
15
|
+
export type DbWorkerInput = DbWorkerInputInit | DbWorkerInputQuery | DbWorkerInputMutate | DbWorkerInputSync | DbWorkerInputReset | DbWorkerInputEnsureSchema | SyncWorkerOutputSyncResponse;
|
|
16
|
+
interface DbWorkerInputInit {
|
|
17
|
+
readonly _tag: "init";
|
|
18
|
+
readonly config: Config;
|
|
19
|
+
readonly tables: Tables;
|
|
20
|
+
}
|
|
21
|
+
interface DbWorkerInputQuery {
|
|
22
|
+
readonly _tag: "query";
|
|
23
|
+
readonly queries: ReadonlyArray.NonEmptyReadonlyArray<Query>;
|
|
24
|
+
}
|
|
25
|
+
interface DbWorkerInputMutate {
|
|
26
|
+
readonly _tag: "mutate";
|
|
27
|
+
readonly items: ReadonlyArray.NonEmptyReadonlyArray<MutateItem>;
|
|
28
|
+
readonly queries: ReadonlyArray<Query>;
|
|
29
|
+
}
|
|
30
|
+
interface DbWorkerInputSync {
|
|
31
|
+
readonly _tag: "sync";
|
|
32
|
+
readonly queries: ReadonlyArray<Query>;
|
|
33
|
+
}
|
|
34
|
+
interface DbWorkerInputReset {
|
|
35
|
+
readonly _tag: "reset";
|
|
36
|
+
readonly mnemonic?: Mnemonic;
|
|
37
|
+
}
|
|
38
|
+
interface DbWorkerInputEnsureSchema {
|
|
39
|
+
readonly _tag: "ensureSchema";
|
|
40
|
+
readonly tables: Tables;
|
|
41
|
+
}
|
|
42
|
+
export type DbWorkerOutput = DbWorkerOutputOnError | DbWorkerOutputOnOwner | DbWorkerOutputOnQuery | DbWorkerOutputOnReceive | DbWorkerOutputOnResetOrRestore | DbWorkerOutputOnSyncState;
|
|
43
|
+
interface DbWorkerOutputOnError {
|
|
44
|
+
readonly _tag: "onError";
|
|
45
|
+
readonly error: EvoluError;
|
|
46
|
+
}
|
|
47
|
+
interface DbWorkerOutputOnOwner {
|
|
48
|
+
readonly _tag: "onOwner";
|
|
49
|
+
readonly owner: Owner;
|
|
50
|
+
}
|
|
51
|
+
export interface DbWorkerOutputOnQuery {
|
|
52
|
+
readonly _tag: "onQuery";
|
|
53
|
+
readonly queriesPatches: ReadonlyArray<QueryPatches>;
|
|
54
|
+
readonly onCompleteIds: ReadonlyArray<OnCompleteId>;
|
|
55
|
+
}
|
|
56
|
+
export type OnCompleteId = string & Brand.Brand<"Id"> & Brand.Brand<"OnComplete">;
|
|
57
|
+
interface DbWorkerOutputOnReceive {
|
|
58
|
+
readonly _tag: "onReceive";
|
|
59
|
+
}
|
|
60
|
+
interface DbWorkerOutputOnResetOrRestore {
|
|
61
|
+
readonly _tag: "onResetOrRestore";
|
|
62
|
+
}
|
|
63
|
+
interface DbWorkerOutputOnSyncState {
|
|
64
|
+
readonly _tag: "onSyncState";
|
|
65
|
+
readonly state: SyncState;
|
|
66
|
+
}
|
|
67
|
+
export interface MutateItem {
|
|
68
|
+
readonly table: string;
|
|
69
|
+
readonly id: Id;
|
|
70
|
+
readonly values: ReadonlyRecord.ReadonlyRecord<Value | Date | boolean | undefined>;
|
|
71
|
+
readonly isInsert: boolean;
|
|
72
|
+
readonly now: SqliteDate;
|
|
73
|
+
readonly onCompleteId: OnCompleteId | null;
|
|
74
|
+
}
|
|
75
|
+
export type RowsCacheMap = ReadonlyMap<Query, ReadonlyArray<Row>>;
|
|
76
|
+
export declare const DbWorkerLive: Layer.Layer<Bip39 | NanoId | Sqlite | SyncWorker, never, DbWorker>;
|
|
77
|
+
export {};
|
|
78
|
+
//# sourceMappingURL=DbWorker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DbWorker.d.ts","sourceRoot":"","sources":["../../src/DbWorker.ts"],"names":[],"mappings":"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"}
|