@playfast/reform-db-sqlite 1.0.2 → 1.2.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/README.md CHANGED
@@ -44,9 +44,7 @@ const DataLayer = QueriesAndProcedures.pipe(
44
44
  )
45
45
 
46
46
  // Ephemeral in-memory — the default for tests / proofs:
47
- const TestLayer = QueriesAndProcedures.pipe(
48
- Layer.provideMerge(Sqlite.memory({ migrations })),
49
- )
47
+ const TestLayer = QueriesAndProcedures.pipe(Layer.provideMerge(Sqlite.memory({ migrations })))
50
48
  ```
51
49
 
52
50
  Both layers build the `Db` service **synchronously** (bun:sqlite opens on a background
@@ -0,0 +1,9 @@
1
+ import { type DbEngine, type Migration } from '@playfast/reform-db';
2
+ export interface SqliteOptionsExternalApi {
3
+ readonly filename?: string;
4
+ readonly migrations?: ReadonlyArray<Migration.Migration>;
5
+ readonly loadDataDir?: Blob;
6
+ }
7
+ export type SqliteOptions = SqliteOptionsExternalApi;
8
+ export declare const make: (options: SqliteOptions) => DbEngine;
9
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAA+B,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAQhG,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAE1B,QAAQ,CAAC,UAAU,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,SAAS,CAAC,CAAA;IACxD,QAAQ,CAAC,WAAW,CAAC,EAAE,IAAI,CAAA;CAC5B;AAED,MAAM,MAAM,aAAa,GAAG,wBAAwB,CAAA;AAiFpD,eAAO,MAAM,IAAI,GAAI,SAAS,aAAa,KAAG,QAG5C,CAAA"}
package/dist/engine.js ADDED
@@ -0,0 +1,69 @@
1
+ import { DbError } from '@playfast/reform-db';
2
+ import { Effect, Option } from 'effect';
3
+ import { createQueryId, SqliteQueryCompiler } from 'kysely';
4
+ const compiler = new SqliteQueryCompiler();
5
+ const compile = (node) => {
6
+ const compiled = compiler.compileQuery(node, createQueryId());
7
+ return { sql: compiled.sql, parameters: compiled.parameters };
8
+ };
9
+ const bindable = (candidate) => candidate instanceof Uint8Array ||
10
+ typeof candidate === 'string' ||
11
+ typeof candidate === 'number' ||
12
+ typeof candidate === 'bigint' ||
13
+ typeof candidate === 'boolean';
14
+ // SQLite throws on object binds — serialize json-column objects/arrays to text.
15
+ const coerceParam = (param) => {
16
+ if (param === undefined || param === null) {
17
+ return null;
18
+ }
19
+ // oxlint-disable-next-line reform-rules/no-json-parse-stringify -- encodes an already-decoded json-column value for SQLite's text storage; the column's own Schema round-trips it on read
20
+ return bindable(param) ? param : JSON.stringify(param);
21
+ };
22
+ // Lazy bun:sqlite import + migrations: async-shaped so makeDriver acquires Db sync.
23
+ const openSqlite = Effect.fn('openSqlite')(function* (options) {
24
+ const { Database } = yield* Effect.tryPromise({
25
+ try: () => import('bun:sqlite'),
26
+ catch: (cause) => new DbError({ reason: 'bun:sqlite unavailable (Bun runtime required)', cause }),
27
+ });
28
+ // deserialize wants bytes sync; Blob is already in memory — read async then open sync.
29
+ const seed = yield* Option.match(Option.fromNullable(options.loadDataDir), {
30
+ onNone: () => Effect.succeed(Option.none()),
31
+ onSome: (blob) => Effect.tryPromise({
32
+ try: () => blob.arrayBuffer(),
33
+ catch: (cause) => new DbError({ reason: 'loadDataDir read failed', cause }),
34
+ }).pipe(Effect.map((buffer) => Option.some(new Uint8Array(buffer)))),
35
+ });
36
+ const db = yield* Effect.try({
37
+ try: () => Option.match(seed, {
38
+ onNone: () => new Database(options.filename ?? ':memory:'),
39
+ onSome: (bytes) => Database.deserialize(bytes),
40
+ }),
41
+ catch: (cause) => new DbError({ reason: 'open failed', cause }),
42
+ });
43
+ yield* Option.match(Option.fromNullable(options.migrations), {
44
+ onNone: () => Effect.void,
45
+ onSome: (migrations) => Effect.forEach(migrations, (migration) => Effect.forEach(migration.statements, (statement) => Effect.try({
46
+ try: () => db.run(statement),
47
+ catch: (cause) => new DbError({ reason: 'migration failed', sql: statement, cause }),
48
+ }))),
49
+ });
50
+ return db;
51
+ });
52
+ const backendOf = (db) => ({
53
+ query: (sql, parameters) => Effect.try({
54
+ try: () => db.query(sql).all(...parameters.map(coerceParam)),
55
+ catch: (cause) => new DbError({ reason: 'query failed', sql, cause }),
56
+ }),
57
+ // No native live — makeDriver emulates by re-running after write.
58
+ live: Option.none(),
59
+ dump: Effect.try({
60
+ try: () => new Blob([new Uint8Array(db.serialize())]),
61
+ catch: (cause) => new DbError({ reason: 'dump failed', cause }),
62
+ }),
63
+ close: Effect.sync(() => db.close()),
64
+ });
65
+ export const make = (options) => ({
66
+ compile,
67
+ open: () => openSqlite(options).pipe(Effect.map(backendOf)),
68
+ });
69
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export * as Sqlite from './sqlite.js';
2
+ export type { MemoryOptions } from './sqlite.js';
3
+ export type { SqliteOptions } from './engine.js';
4
+ export { make as makeSqliteEngine } from './engine.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,UAAU,CAAA;AAClC,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7C,YAAY,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7C,OAAO,EAAE,IAAI,IAAI,gBAAgB,EAAE,MAAM,UAAU,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * as Sqlite from './sqlite.js';
2
+ export { make as makeSqliteEngine } from './engine.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,UAAU,CAAA;AAGlC,OAAO,EAAE,IAAI,IAAI,gBAAgB,EAAE,MAAM,UAAU,CAAA"}
@@ -0,0 +1,11 @@
1
+ import { Db } from '@playfast/reform-db';
2
+ import { Layer } from 'effect';
3
+ import { type SqliteOptions } from './engine.js';
4
+ export interface MemoryOptionsExternalApi {
5
+ readonly migrations?: SqliteOptions['migrations'];
6
+ readonly loadDataDir?: SqliteOptions['loadDataDir'];
7
+ }
8
+ export type MemoryOptions = MemoryOptionsExternalApi;
9
+ export declare const layer: (options?: SqliteOptions) => Layer.Layer<Db>;
10
+ export declare const memory: (options?: MemoryOptions) => Layer.Layer<Db>;
11
+ //# sourceMappingURL=sqlite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../src/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAc,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAC9B,OAAO,EAAQ,KAAK,aAAa,EAAE,MAAM,UAAU,CAAA;AAEnD,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,UAAU,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAA;IACjD,QAAQ,CAAC,WAAW,CAAC,EAAE,aAAa,CAAC,aAAa,CAAC,CAAA;CACpD;AAED,MAAM,MAAM,aAAa,GAAG,wBAAwB,CAAA;AAEpD,eAAO,MAAM,KAAK,GAAI,UAAS,aAAkB,KAAG,KAAK,CAAC,KAAK,CAAC,EAAE,CACrB,CAAA;AAE7C,eAAO,MAAM,MAAM,GAAI,UAAS,aAAkB,KAAG,KAAK,CAAC,KAAK,CAAC,EAAE,CAUhE,CAAA"}
package/dist/sqlite.js ADDED
@@ -0,0 +1,11 @@
1
+ import { Db, makeDriver } from '@playfast/reform-db';
2
+ import { Layer } from 'effect';
3
+ import { make } from './engine.js';
4
+ export const layer = (options = {}) => Layer.scoped(Db, makeDriver(make(options)));
5
+ export const memory = (options = {}) => Layer.scoped(Db,
6
+ // Spread only when set so bare `Sqlite.memory()` open stays unchanged.
7
+ makeDriver(make({
8
+ ...(options.migrations !== undefined ? { migrations: options.migrations } : {}),
9
+ ...(options.loadDataDir !== undefined ? { loadDataDir: options.loadDataDir } : {}),
10
+ })));
11
+ //# sourceMappingURL=sqlite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../src/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAC9B,OAAO,EAAE,IAAI,EAAsB,MAAM,UAAU,CAAA;AASnD,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,UAAyB,EAAE,EAAmB,EAAE,CACpE,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;AAE7C,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,UAAyB,EAAE,EAAmB,EAAE,CACrE,KAAK,CAAC,MAAM,CACV,EAAE;AACF,uEAAuE;AACvE,UAAU,CACR,IAAI,CAAC;IACH,GAAG,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/E,GAAG,CAAC,OAAO,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;CACnF,CAAC,CACH,CACF,CAAA"}
package/package.json CHANGED
@@ -1,55 +1,67 @@
1
1
  {
2
2
  "name": "@playfast/reform-db-sqlite",
3
- "playbook": "./playbook",
4
- "version": "1.0.2",
5
- "type": "module",
3
+ "version": "1.2.0",
6
4
  "description": "bun:sqlite engine for @playfast/reform-db — a Db driver backed by Bun's built-in SQLite, with emulated reactivity and serialize()/deserialize() snapshots.",
7
5
  "keywords": [
8
- "reform",
9
- "effect",
10
- "sqlite",
11
6
  "bun",
12
- "sql",
7
+ "effect",
8
+ "local-first",
13
9
  "reactive",
14
- "local-first"
10
+ "reform",
11
+ "sql",
12
+ "sqlite"
15
13
  ],
14
+ "bugs": {
15
+ "url": "https://github.com/playfast/reform/issues"
16
+ },
16
17
  "license": "MIT",
17
18
  "repository": {
18
19
  "type": "git",
19
20
  "url": "https://github.com/playfast/reform.git",
20
21
  "directory": "packages/reform-db-sqlite"
21
22
  },
22
- "bugs": {
23
- "url": "https://github.com/playfast/reform/issues"
24
- },
25
- "sideEffects": false,
26
- "exports": {
27
- "./package.json": "./package.json",
28
- ".": "./src/index.ts",
29
- "./*": "./src/*.ts"
30
- },
31
23
  "files": [
24
+ "dist",
32
25
  "src",
33
26
  "README.md"
34
27
  ],
28
+ "type": "module",
29
+ "sideEffects": false,
30
+ "main": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ "./package.json": "./package.json",
34
+ ".": {
35
+ "playfast-src": "./src/index.ts",
36
+ "types": "./dist/index.d.ts",
37
+ "default": "./dist/index.js"
38
+ },
39
+ "./*": {
40
+ "playfast-src": "./src/*.ts",
41
+ "types": "./dist/*.d.ts",
42
+ "default": "./dist/*.js"
43
+ }
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
35
48
  "scripts": {
36
49
  "clean": "rm -rf dist .tsbuildinfo",
37
50
  "check": "tsc --noEmit",
38
- "build": "tsc -p tsconfig.build.json",
51
+ "build": "rm -rf dist .tsbuildinfo && tsc -p tsconfig.build.json && bun ../../scripts/fix-esm-extensions.ts dist",
39
52
  "test": "bun --bun run vitest run --config vitest.config.ts",
40
53
  "lint": "oxlint src",
41
- "lint:fix": "oxlint --fix src"
42
- },
43
- "peerDependencies": {
44
- "effect": "*",
45
- "kysely": "*",
46
- "@playfast/reform": "*",
47
- "@playfast/reform-db": "*"
54
+ "lint:fix": "oxlint --fix src",
55
+ "prepack": "bun run build"
48
56
  },
49
57
  "devDependencies": {
50
58
  "@types/bun": "^1.3.14"
51
59
  },
52
- "publishConfig": {
53
- "access": "public"
54
- }
60
+ "peerDependencies": {
61
+ "@playfast/reform": "*",
62
+ "@playfast/reform-db": "*",
63
+ "effect": "*",
64
+ "kysely": "*"
65
+ },
66
+ "playbook": "./playbook"
55
67
  }
package/src/engine.ts CHANGED
@@ -44,7 +44,8 @@ const openSqlite = Effect.fn('openSqlite')(function* (
44
44
  ): Effect.fn.Return<Database, DbError> {
45
45
  const { Database } = yield* Effect.tryPromise({
46
46
  try: () => import('bun:sqlite'),
47
- catch: (cause) => new DbError({ reason: 'bun:sqlite unavailable (Bun runtime required)', cause }),
47
+ catch: (cause) =>
48
+ new DbError({ reason: 'bun:sqlite unavailable (Bun runtime required)', cause }),
48
49
  })
49
50
  // deserialize wants bytes sync; Blob is already in memory — read async then open sync.
50
51
  const seed = yield* Option.match(Option.fromNullable(options.loadDataDir), {
@@ -7,14 +7,14 @@ import {
7
7
  DbTable,
8
8
  Migration,
9
9
  type QueryData,
10
- } from "@playfast/reform-db";
11
- import { expect, it } from "@effect/vitest";
12
- import { Context, Effect, Exit, Layer, Schedule, Schema, Scope } from "effect";
13
- import { Sqlite } from "./index";
10
+ } from '@playfast/reform-db'
11
+ import { expect, it } from '@effect/vitest'
12
+ import { Context, Effect, Exit, Layer, Schedule, Schema, Scope } from 'effect'
13
+ import { Sqlite } from './index'
14
14
 
15
- const Meta = Schema.Struct({ note: Schema.String });
15
+ const Meta = Schema.Struct({ note: Schema.String })
16
16
 
17
- const Items = DbTable.make("items", {
17
+ const Items = DbTable.make('items', {
18
18
  columns: {
19
19
  id: DbColumn.text({ primaryKey: true }),
20
20
  label: DbColumn.text(),
@@ -22,130 +22,109 @@ const Items = DbTable.make("items", {
22
22
  done: DbColumn.boolean(),
23
23
  meta: DbColumn.json(Meta),
24
24
  },
25
- primaryKey: "id",
26
- });
25
+ primaryKey: 'id',
26
+ })
27
27
 
28
- const AppDb = DbSchema.make("app", { tables: [Items] });
29
- const migrations = Migration.fromSchema(AppDb, Migration.sqlite);
28
+ const AppDb = DbSchema.make('app', { tables: [Items] })
29
+ const migrations = Migration.fromSchema(AppDb, Migration.sqlite)
30
30
 
31
- const ItemsQuery = DbQuery.make("items", { output: Items.Row });
31
+ const ItemsQuery = DbQuery.make('items', { output: Items.Row })
32
32
 
33
33
  type Item = {
34
- readonly id: string;
35
- readonly label: string;
36
- readonly rank: number;
37
- readonly done: boolean;
38
- readonly meta: { readonly note: string };
39
- };
34
+ readonly id: string
35
+ readonly label: string
36
+ readonly rank: number
37
+ readonly done: boolean
38
+ readonly meta: { readonly note: string }
39
+ }
40
40
 
41
41
  const rowsOf = (data: QueryData<Item>): ReadonlyArray<Item> =>
42
- data._tag === "Success" ? data.value : [];
42
+ data._tag === 'Success' ? data.value : []
43
43
 
44
44
  const waitUntil = (predicate: () => boolean) =>
45
45
  Effect.sync(predicate).pipe(
46
46
  Effect.repeat({
47
47
  until: (ready) => ready,
48
- schedule: Schedule.spaced("20 millis"),
48
+ schedule: Schedule.spaced('20 millis'),
49
49
  }),
50
- Effect.timeout("5 seconds"),
51
- );
50
+ Effect.timeout('5 seconds'),
51
+ )
52
52
 
53
53
  const liveLayer = (dataLayer: Layer.Layer<Db>) =>
54
54
  DbQuery.live(ItemsQuery, {
55
55
  schema: AppDb,
56
56
  inputs: [],
57
- build: (_inputs, db) => db.selectFrom("items").selectAll().orderBy("rank"),
58
- }).pipe(Layer.provideMerge(dataLayer));
59
-
60
- it.live(
61
- "DbQuery reflects writes (emulated reactivity), decoding boolean + json columns",
62
- () => {
63
- const program = Effect.gen(function* () {
64
- const store = yield* ItemsQuery.store;
65
-
66
- yield* waitUntil(() => store.getSnapshot()._tag === "Success");
67
- expect(rowsOf(store.getSnapshot())).toEqual([]);
68
-
69
- const insert = (
70
- id: string,
71
- label: string,
72
- rank: number,
73
- done: boolean,
74
- note: string,
75
- ) =>
76
- Db.exec(
77
- AppDb.kysely
78
- .insertInto("items")
79
- .values({ id, label, rank, done, meta: { note } }),
80
- );
81
-
82
- yield* insert("b", "second", 2, false, "two");
83
- yield* insert("a", "first", 1, true, "one");
84
-
85
- yield* waitUntil(() => rowsOf(store.getSnapshot()).length === 2);
86
- const rows = rowsOf(store.getSnapshot());
87
- expect(rows.map((row) => row.label)).toEqual(["first", "second"]);
88
- expect(rows.map((row) => row.done)).toEqual([true, false]);
89
- expect(rows.map((row) => row.meta.note)).toEqual(["one", "two"]);
90
- }).pipe(Effect.provide(liveLayer(Sqlite.memory({ migrations }))));
91
-
92
- return program;
93
- },
94
- );
95
-
96
- it.scopedLive(
97
- "dump() → loadDataDir round-trips rows into a fresh in-memory db",
98
- () =>
99
- Effect.gen(function* () {
100
- const dump = yield* Effect.scoped(
101
- Effect.gen(function* () {
102
- const context = yield* Layer.build(Sqlite.memory({ migrations }));
103
- const driver = Context.get(context, Db);
104
- yield* Db.exec(
105
- AppDb.kysely
106
- .insertInto("items")
107
- .values({
108
- id: "a",
109
- label: "first",
110
- rank: 1,
111
- done: true,
112
- meta: { note: "one" },
113
- }),
114
- ).pipe(Effect.provideService(Db, driver));
115
- return yield* driver.dump();
116
- }),
117
- );
118
-
119
- yield* Effect.scoped(
120
- Effect.gen(function* () {
121
- const store = yield* ItemsQuery.store;
122
- yield* waitUntil(() => rowsOf(store.getSnapshot()).length === 1);
123
- const row = rowsOf(store.getSnapshot())[0];
124
- expect(row?.label).toBe("first");
125
- expect(row?.done).toBe(true);
126
- expect(row?.meta.note).toBe("one");
127
- }).pipe(
128
- Effect.provide(
129
- liveLayer(Sqlite.memory({ migrations, loadDataDir: dump })),
130
- ),
131
- ),
132
- );
133
- }),
134
- );
57
+ build: (_inputs, db) => db.selectFrom('items').selectAll().orderBy('rank'),
58
+ }).pipe(Layer.provideMerge(dataLayer))
59
+
60
+ it.live('DbQuery reflects writes (emulated reactivity), decoding boolean + json columns', () => {
61
+ const program = Effect.gen(function* () {
62
+ const store = yield* ItemsQuery.store
63
+
64
+ yield* waitUntil(() => store.getSnapshot()._tag === 'Success')
65
+ expect(rowsOf(store.getSnapshot())).toEqual([])
66
+
67
+ const insert = (id: string, label: string, rank: number, done: boolean, note: string) =>
68
+ Db.exec(AppDb.kysely.insertInto('items').values({ id, label, rank, done, meta: { note } }))
69
+
70
+ yield* insert('b', 'second', 2, false, 'two')
71
+ yield* insert('a', 'first', 1, true, 'one')
72
+
73
+ yield* waitUntil(() => rowsOf(store.getSnapshot()).length === 2)
74
+ const rows = rowsOf(store.getSnapshot())
75
+ expect(rows.map((row) => row.label)).toEqual(['first', 'second'])
76
+ expect(rows.map((row) => row.done)).toEqual([true, false])
77
+ expect(rows.map((row) => row.meta.note)).toEqual(['one', 'two'])
78
+ }).pipe(Effect.provide(liveLayer(Sqlite.memory({ migrations }))))
79
+
80
+ return program
81
+ })
82
+
83
+ it.scopedLive('dump() → loadDataDir round-trips rows into a fresh in-memory db', () =>
84
+ Effect.gen(function* () {
85
+ const dump = yield* Effect.scoped(
86
+ Effect.gen(function* () {
87
+ const context = yield* Layer.build(Sqlite.memory({ migrations }))
88
+ const driver = Context.get(context, Db)
89
+ yield* Db.exec(
90
+ AppDb.kysely.insertInto('items').values({
91
+ id: 'a',
92
+ label: 'first',
93
+ rank: 1,
94
+ done: true,
95
+ meta: { note: 'one' },
96
+ }),
97
+ ).pipe(Effect.provideService(Db, driver))
98
+ return yield* driver.dump()
99
+ }),
100
+ )
101
+
102
+ yield* Effect.scoped(
103
+ Effect.gen(function* () {
104
+ const store = yield* ItemsQuery.store
105
+ yield* waitUntil(() => rowsOf(store.getSnapshot()).length === 1)
106
+ const row = rowsOf(store.getSnapshot())[0]
107
+ expect(row?.label).toBe('first')
108
+ expect(row?.done).toBe(true)
109
+ expect(row?.meta.note).toBe('one')
110
+ }).pipe(Effect.provide(liveLayer(Sqlite.memory({ migrations, loadDataDir: dump })))),
111
+ )
112
+ }),
113
+ )
135
114
 
136
115
  it.scopedLive(
137
- "Sqlite.memory layer is sync-providable: store starts loading, then resolves to Success",
116
+ 'Sqlite.memory layer is sync-providable: store starts loading, then resolves to Success',
138
117
  () =>
139
118
  Effect.gen(function* () {
140
- const layer = liveLayer(Sqlite.memory({ migrations }));
119
+ const layer = liveLayer(Sqlite.memory({ migrations }))
141
120
 
142
121
  // Sync build (opens on background fiber) — must not throw "Fiber cannot be resolved synchronously".
143
- const scope = Effect.runSync(Scope.make());
144
- const context = Effect.runSync(Layer.buildWithScope(layer, scope));
145
- yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void));
146
- const store = Context.get(context, ItemsQuery.store);
147
- expect(store.getSnapshot()._tag).toBe("Loading");
148
- yield* waitUntil(() => store.getSnapshot()._tag === "Success");
149
- expect(rowsOf(store.getSnapshot())).toEqual([]);
122
+ const scope = Effect.runSync(Scope.make())
123
+ const context = Effect.runSync(Layer.buildWithScope(layer, scope))
124
+ yield* Effect.addFinalizer(() => Scope.close(scope, Exit.void))
125
+ const store = Context.get(context, ItemsQuery.store)
126
+ expect(store.getSnapshot()._tag).toBe('Loading')
127
+ yield* waitUntil(() => store.getSnapshot()._tag === 'Success')
128
+ expect(rowsOf(store.getSnapshot())).toEqual([])
150
129
  }),
151
- );
130
+ )