tablinum 0.0.1
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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.context/attachments/pasted_text_2026-03-07_14-02-40.txt +571 -0
- package/.context/attachments/pasted_text_2026-03-07_15-48-27.txt +498 -0
- package/.context/notes.md +0 -0
- package/.context/plans/add-changesets-to-douala-v4.md +48 -0
- package/.context/plans/dexie-js-style-query-language-for-localstr.md +115 -0
- package/.context/plans/dexie-js-style-query-language-with-per-collection-.md +336 -0
- package/.context/plans/implementation-plan-localstr-v0-2.md +263 -0
- package/.context/plans/project-init-effect-v4-bun-oxlint-oxfmt-vitest.md +71 -0
- package/.context/plans/revise-localstr-prd-v0-2.md +132 -0
- package/.context/plans/svelte-5-runes-bindings-for-localstr.md +233 -0
- package/.context/todos.md +0 -0
- package/.github/workflows/release.yml +36 -0
- package/.oxlintrc.json +8 -0
- package/README.md +1 -0
- package/bun.lock +705 -0
- package/examples/svelte/bun.lock +261 -0
- package/examples/svelte/package.json +21 -0
- package/examples/svelte/src/app.html +11 -0
- package/examples/svelte/src/lib/db.ts +44 -0
- package/examples/svelte/src/routes/+page.svelte +322 -0
- package/examples/svelte/svelte.config.js +16 -0
- package/examples/svelte/tsconfig.json +6 -0
- package/examples/svelte/vite.config.ts +6 -0
- package/examples/vanilla/app.ts +219 -0
- package/examples/vanilla/index.html +144 -0
- package/examples/vanilla/serve.ts +42 -0
- package/package.json +46 -0
- package/prds/localstr-v0.2.md +221 -0
- package/prek.toml +10 -0
- package/scripts/validate.ts +392 -0
- package/src/crud/collection-handle.ts +189 -0
- package/src/crud/query-builder.ts +414 -0
- package/src/crud/watch.ts +78 -0
- package/src/db/create-localstr.ts +217 -0
- package/src/db/database-handle.ts +16 -0
- package/src/db/identity.ts +49 -0
- package/src/errors.ts +37 -0
- package/src/index.ts +32 -0
- package/src/main.ts +10 -0
- package/src/schema/collection.ts +53 -0
- package/src/schema/field.ts +25 -0
- package/src/schema/types.ts +19 -0
- package/src/schema/validate.ts +111 -0
- package/src/storage/events-store.ts +24 -0
- package/src/storage/giftwraps-store.ts +23 -0
- package/src/storage/idb.ts +244 -0
- package/src/storage/lww.ts +17 -0
- package/src/storage/records-store.ts +76 -0
- package/src/svelte/collection.svelte.ts +87 -0
- package/src/svelte/database.svelte.ts +83 -0
- package/src/svelte/index.svelte.ts +52 -0
- package/src/svelte/live-query.svelte.ts +29 -0
- package/src/svelte/query.svelte.ts +101 -0
- package/src/sync/gift-wrap.ts +33 -0
- package/src/sync/negentropy.ts +83 -0
- package/src/sync/publish-queue.ts +61 -0
- package/src/sync/relay.ts +239 -0
- package/src/sync/sync-service.ts +183 -0
- package/src/sync/sync-status.ts +17 -0
- package/src/utils/uuid.ts +22 -0
- package/src/vendor/negentropy.js +616 -0
- package/tests/db/create-localstr.test.ts +174 -0
- package/tests/db/identity.test.ts +33 -0
- package/tests/main.test.ts +9 -0
- package/tests/schema/collection.test.ts +27 -0
- package/tests/schema/field.test.ts +41 -0
- package/tests/schema/validate.test.ts +85 -0
- package/tests/setup.ts +1 -0
- package/tests/storage/idb.test.ts +144 -0
- package/tests/storage/lww.test.ts +33 -0
- package/tests/sync/gift-wrap.test.ts +56 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Plan: Revise localstr PRD v0.2
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
The current PRD (`prds/localstr-v0.2.md`) has been critiqued and several design decisions have been made through discussion. The goal is to produce a revised PRD that incorporates all agreed-upon changes. This is a document revision task — no code changes.
|
|
6
|
+
|
|
7
|
+
## Decisions Made
|
|
8
|
+
|
|
9
|
+
1. **Effect as the core public API** — The library exports only an Effect API. Framework bindings (Svelte, React, etc.) come later as separate packages. No Promise wrapper in the core.
|
|
10
|
+
|
|
11
|
+
2. **NIP-59 (Gift Wrap) + NIP-77 (Negentropy) from day one** — Full privacy stack. Events are triple-wrapped (rumor → seal → gift wrap) with randomized timestamps and disposable sender keys. Sync uses negentropy set reconciliation instead of timestamp-based `since` filters.
|
|
12
|
+
|
|
13
|
+
3. **LWW conflict resolution** — Last-write-wins by `created_at`, ties broken by lowest event ID (lexicographic).
|
|
14
|
+
|
|
15
|
+
4. **`d` tag = `{collectionName}:{recordId}`** with UUIDv7 record IDs. This is client-side only (relay never sees it due to NIP-59 encryption).
|
|
16
|
+
|
|
17
|
+
5. **Remove KV API** — Cut from v0.2 entirely.
|
|
18
|
+
|
|
19
|
+
6. **No schema migration in v0.2** — New optional fields are safe. Required field changes require manual rebuild. Document this as a known limitation.
|
|
20
|
+
|
|
21
|
+
7. **Full query API** — Ship all query operators: `where().equals()`, `above()`, `below()`, `between()`, `anyOf()`, `sortBy()`, `limit()`, `offset()`.
|
|
22
|
+
|
|
23
|
+
8. **NIP-77 for sync/dedup** — Negentropy handles cross-relay deduplication naturally. No timestamp-based sync checkpoints.
|
|
24
|
+
|
|
25
|
+
9. **Storage limits** — Document that the library doesn't manage IndexedDB quota. Suggest `navigator.storage.persist()`. No compaction in v0.2.
|
|
26
|
+
|
|
27
|
+
10. **`db.close()` lifecycle** — Add to the API for releasing IndexedDB connections and cleaning up subscriptions.
|
|
28
|
+
|
|
29
|
+
11. **Identity: `exportKey()` + import via config** — Include key export/import in v0.2 since it's required for the cross-device sync success metric.
|
|
30
|
+
|
|
31
|
+
12. **`watch()` returns an `Effect.Stream`** — Emits initial results then subsequent changes. During sync replay, batch changes and emit once after replay completes.
|
|
32
|
+
|
|
33
|
+
## Key Changes to the PRD
|
|
34
|
+
|
|
35
|
+
### Structural changes
|
|
36
|
+
|
|
37
|
+
- **Remove US-008** (hiding Effect from public API) — replaced by the opposite: Effect IS the public API
|
|
38
|
+
- **Add US for key export/import** or fold into US-002
|
|
39
|
+
- **Remove KV from US-002** database handle description
|
|
40
|
+
- **Add `close()` to US-002** acceptance criteria
|
|
41
|
+
|
|
42
|
+
### US-004 (CRUD) changes
|
|
43
|
+
|
|
44
|
+
- `watch()` returns `Effect.Stream<ReadonlyArray<Record>>`
|
|
45
|
+
- Specify that all methods return `Effect` values with typed error channels
|
|
46
|
+
|
|
47
|
+
### US-005 (Query) changes
|
|
48
|
+
|
|
49
|
+
- Full query API (no tiering)
|
|
50
|
+
- `watch()` batches changes during sync replay
|
|
51
|
+
|
|
52
|
+
### US-006 (Sync) changes — Major rewrite
|
|
53
|
+
|
|
54
|
+
- Replace "encrypt event payloads" with NIP-59 gift wrap (3-layer: rumor → seal → gift wrap)
|
|
55
|
+
- Replace "parameterized replaceable event kind" with gift-wrapped events (kind 1059)
|
|
56
|
+
- Replace "deterministic `d` tag" — still deterministic but client-side only, invisible to relay
|
|
57
|
+
- Replace "scoped checkpoints" with NIP-77 negentropy set reconciliation
|
|
58
|
+
- Add: "LWW by `created_at`, ties broken by lowest event ID"
|
|
59
|
+
- Add: relay stores every version (no relay-side deduplication)
|
|
60
|
+
- Add: client-side dedup and latest-version resolution after decryption
|
|
61
|
+
|
|
62
|
+
### US-007 (Offline) changes
|
|
63
|
+
|
|
64
|
+
- Queue refers to gift-wrapped events, not raw events
|
|
65
|
+
|
|
66
|
+
### US-008 → Delete
|
|
67
|
+
|
|
68
|
+
- The old "conventional public error model" story needs rewriting since errors are now in Effect's error channel
|
|
69
|
+
- Fold error types into a new story or into Technical Considerations
|
|
70
|
+
|
|
71
|
+
### New/revised Functional Requirements
|
|
72
|
+
|
|
73
|
+
- Remove FR-20 (hide Nostr from CRUD API) — still true but reframe: the Effect API is the primary API, framework bindings hide everything
|
|
74
|
+
- Add FR for NIP-59 gift wrapping
|
|
75
|
+
- Add FR for NIP-77 negentropy sync
|
|
76
|
+
- Add FR for LWW conflict resolution
|
|
77
|
+
- Add FR for key export
|
|
78
|
+
- Add FR for `close()` lifecycle
|
|
79
|
+
- Remove any mention of KV
|
|
80
|
+
|
|
81
|
+
### Technical Considerations updates
|
|
82
|
+
|
|
83
|
+
- Effect is the public API; framework bindings are separate packages
|
|
84
|
+
- NIP-59 for encryption (not NIP-44 directly — NIP-44 is used _within_ NIP-59)
|
|
85
|
+
- NIP-77 for sync (not `since`-based filtering)
|
|
86
|
+
- UUIDv7 for record IDs
|
|
87
|
+
- `d` tag format: `{collectionName}:{recordId}` (client-side only)
|
|
88
|
+
- No schema migration; new optional fields safe, other changes require rebuild
|
|
89
|
+
- No IndexedDB quota management; recommend `navigator.storage.persist()`
|
|
90
|
+
- Relay is untrusted; learns almost nothing about data structure
|
|
91
|
+
|
|
92
|
+
### Non-Goals updates
|
|
93
|
+
|
|
94
|
+
- Add: Promise-based public API (that's for framework bindings)
|
|
95
|
+
- Add: Schema migration
|
|
96
|
+
- Add: KV store
|
|
97
|
+
- Add: IndexedDB quota management / compaction
|
|
98
|
+
- Keep existing non-goals
|
|
99
|
+
|
|
100
|
+
### Open Questions updates
|
|
101
|
+
|
|
102
|
+
- Remove resolved questions (event kind → kind 1059 gift wraps, export/import → yes, autoSync → manual only, KV → cut, Effect adapter → Effect is the core API)
|
|
103
|
+
- New open questions:
|
|
104
|
+
- Should there be a `rebuild()` method that replays all local events to regenerate materialized state?
|
|
105
|
+
- What's the maximum reasonable event count before negentropy reconciliation becomes slow in a browser?
|
|
106
|
+
- Should the library include a NIP-77-compatible negentropy implementation or depend on an external one?
|
|
107
|
+
|
|
108
|
+
## IndexedDB Storage Architecture
|
|
109
|
+
|
|
110
|
+
Three stores in IndexedDB:
|
|
111
|
+
|
|
112
|
+
| Store | Contents | Purpose |
|
|
113
|
+
| ----------- | ------------------------------------------------------------------- | ------------------------------------------- |
|
|
114
|
+
| `giftwraps` | Gift wrap event IDs only (not full encrypted blobs) | Negentropy fingerprint computation for sync |
|
|
115
|
+
| `events` | Decrypted rumors (real events with `d` tags, `created_at`, content) | Source of truth, replay, rebuild |
|
|
116
|
+
| `records` | Materialized current state per `d` tag (LWW winner) | Query performance |
|
|
117
|
+
|
|
118
|
+
- On local write: create rumor + gift wrap. Store gift wrap ID in `giftwraps`, decrypted rumor in `events`, LWW-resolved state in `records`. Publish gift wrap to relay async.
|
|
119
|
+
- On sync: negentropy over `giftwraps` IDs → fetch missing gift wraps → decrypt → store rumor in `events` + gift wrap ID in `giftwraps` → LWW resolve into `records`.
|
|
120
|
+
- On rebuild: clear `records`, replay all `events` applying LWW, regenerate `records`.
|
|
121
|
+
- `giftwraps` grows with every write (one per mutation). `records` stays at one entry per unique `d` tag.
|
|
122
|
+
|
|
123
|
+
## File to modify
|
|
124
|
+
|
|
125
|
+
- `prds/localstr-v0.2.md` — full rewrite incorporating all changes above
|
|
126
|
+
|
|
127
|
+
## Verification
|
|
128
|
+
|
|
129
|
+
- Read through the revised PRD and confirm all 12 decisions are reflected
|
|
130
|
+
- Confirm no references to KV, Promise wrappers, NIP-04, `since`-based sync, or "hide Effect"
|
|
131
|
+
- Confirm NIP-59, NIP-77, LWW, UUIDv7, `close()`, `exportKey()` are all present
|
|
132
|
+
- Run `bun run validate` to ensure no code breakage (though this is a doc-only change)
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Svelte 5 Runes Bindings for localstr
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
localstr has a fully Effect-based API where all operations return `Effect` and reactive streams return `Stream`. This is powerful but requires Effect knowledge to use. We want Svelte 5 bindings that wrap this API into idiomatic Svelte runes classes — `$state` for reactive data, Promises for CRUD, and auto-cleanup via `$effect`.
|
|
6
|
+
|
|
7
|
+
## API Design
|
|
8
|
+
|
|
9
|
+
```svelte
|
|
10
|
+
<script>
|
|
11
|
+
import { createLocalstr, field, collection } from 'localstr/svelte'
|
|
12
|
+
|
|
13
|
+
const todos = collection("todos", {
|
|
14
|
+
title: field.string(),
|
|
15
|
+
done: field.boolean(),
|
|
16
|
+
priority: field.number(),
|
|
17
|
+
}, { indices: ["done", "priority"] })
|
|
18
|
+
|
|
19
|
+
const db = await createLocalstr({
|
|
20
|
+
schema: { todos },
|
|
21
|
+
relays: ["wss://relay.example.com"],
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const col = db.collection("todos")
|
|
25
|
+
// col.items → $state, auto-watched, all records (live)
|
|
26
|
+
// col.status → $state, 'idle' | 'syncing'
|
|
27
|
+
// col.error → $state, last error or null
|
|
28
|
+
|
|
29
|
+
const incomplete = col.where("done").equals(false).live()
|
|
30
|
+
// incomplete.items → $state, filtered live query
|
|
31
|
+
|
|
32
|
+
// CRUD — returns Promises, sets col.error on failure
|
|
33
|
+
const id = await col.add({ title: "Buy milk", done: false, priority: 1 })
|
|
34
|
+
await col.update(id, { done: true })
|
|
35
|
+
await col.delete(id)
|
|
36
|
+
const todo = await col.get(id)
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
{#if col.error}<p class="error">{col.error.message}</p>{/if}
|
|
40
|
+
{#each incomplete.items as todo}
|
|
41
|
+
<p>{todo.title}</p>
|
|
42
|
+
{/each}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Files to Create
|
|
46
|
+
|
|
47
|
+
### `src/svelte/index.svelte.ts` — Main entry point
|
|
48
|
+
|
|
49
|
+
- Re-exports `field`, `collection` from core (unchanged)
|
|
50
|
+
- Exports wrapped `createLocalstr` that returns `Promise<Database>`
|
|
51
|
+
- Exports `Database`, `Collection`, `LiveQuery` classes
|
|
52
|
+
|
|
53
|
+
### `src/svelte/database.svelte.ts` — Database class
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
class Database<S extends SchemaConfig> {
|
|
57
|
+
#handle: DatabaseHandle<S>
|
|
58
|
+
#scope: Scope.CloseableScope
|
|
59
|
+
#fiber: Fiber<...> | null // for sync status polling
|
|
60
|
+
|
|
61
|
+
status = $state<SyncStatus>('idle')
|
|
62
|
+
error = $state<Error | null>(null)
|
|
63
|
+
|
|
64
|
+
collection<K extends keyof S>(name: K): Collection<S[K]>
|
|
65
|
+
exportKey(): string
|
|
66
|
+
async close(): Promise<void> // closes scope + handle
|
|
67
|
+
async sync(): Promise<void>
|
|
68
|
+
async rebuild(): Promise<void>
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `src/svelte/collection.svelte.ts` — Collection class
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
class Collection<C extends CollectionDef> {
|
|
76
|
+
#handle: CollectionHandle<C>;
|
|
77
|
+
#cleanup: (() => void) | null; // fiber interrupt for watch stream
|
|
78
|
+
|
|
79
|
+
items = $state<ReadonlyArray<InferRecord<C>>>([]);
|
|
80
|
+
error = $state<Error | null>(null);
|
|
81
|
+
|
|
82
|
+
// CRUD — Promise-based, sets this.error on failure
|
|
83
|
+
async add(data): Promise<string>;
|
|
84
|
+
async update(id, data): Promise<void>;
|
|
85
|
+
async delete(id): Promise<void>;
|
|
86
|
+
async get(id): Promise<InferRecord<C>>;
|
|
87
|
+
async first(): Promise<InferRecord<C> | null>;
|
|
88
|
+
async count(): Promise<number>;
|
|
89
|
+
|
|
90
|
+
// Query builders — return Svelte-wrapped builders with .live()
|
|
91
|
+
where(field): SvelteWhereClause<InferRecord<C>>;
|
|
92
|
+
orderBy(field): SvelteOrderByBuilder<InferRecord<C>>;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### `src/svelte/live-query.svelte.ts` — LiveQuery class
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
class LiveQuery<T> {
|
|
100
|
+
items = $state<ReadonlyArray<T>>([]);
|
|
101
|
+
#cleanup: (() => void) | null;
|
|
102
|
+
|
|
103
|
+
constructor(stream: Stream<ReadonlyArray<T>>) {
|
|
104
|
+
// Fork stream processing, update items on each emission
|
|
105
|
+
// Return cleanup function that interrupts the fiber
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
destroy(): void; // manual cleanup if needed
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### `src/svelte/query.svelte.ts` — Svelte query builder wrappers
|
|
113
|
+
|
|
114
|
+
Thin wrappers over the Effect query builders that add `.live()` terminal:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
interface SvelteQueryBuilder<T> {
|
|
118
|
+
and(fn): SvelteQueryBuilder<T>;
|
|
119
|
+
sortBy(field): SvelteQueryBuilder<T>;
|
|
120
|
+
reverse(): SvelteQueryBuilder<T>;
|
|
121
|
+
offset(n): SvelteQueryBuilder<T>;
|
|
122
|
+
limit(n): SvelteQueryBuilder<T>;
|
|
123
|
+
get(): Promise<ReadonlyArray<T>>; // Effect.runPromise
|
|
124
|
+
first(): Promise<T | null>;
|
|
125
|
+
count(): Promise<number>;
|
|
126
|
+
live(): LiveQuery<T>; // NEW: returns reactive LiveQuery
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface SvelteWhereClause<T> {
|
|
130
|
+
equals(value): SvelteQueryBuilder<T>;
|
|
131
|
+
above(value): SvelteQueryBuilder<T>;
|
|
132
|
+
// ... all WhereClause methods, returning SvelteQueryBuilder
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface SvelteOrderByBuilder<T> {
|
|
136
|
+
reverse(): SvelteOrderByBuilder<T>;
|
|
137
|
+
offset(n): SvelteOrderByBuilder<T>;
|
|
138
|
+
limit(n): SvelteOrderByBuilder<T>;
|
|
139
|
+
get(): Promise<ReadonlyArray<T>>;
|
|
140
|
+
first(): Promise<T | null>;
|
|
141
|
+
count(): Promise<number>;
|
|
142
|
+
live(): LiveQuery<T>;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Key Implementation Details
|
|
147
|
+
|
|
148
|
+
### Effect Scope Management
|
|
149
|
+
|
|
150
|
+
`createLocalstr` returns `Effect<DatabaseHandle, ..., Scope.Scope>`. The scope must stay alive until `close()` is called:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// In createLocalstr wrapper:
|
|
154
|
+
const scope = Effect.runSync(Scope.make());
|
|
155
|
+
const handle = await Effect.runPromise(
|
|
156
|
+
coreCreateLocalstr(config).pipe(Effect.provideService(Scope.Scope, scope)),
|
|
157
|
+
);
|
|
158
|
+
// scope is stored in Database, closed on db.close()
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Stream → $state Bridging
|
|
162
|
+
|
|
163
|
+
For auto-watching `items` and `LiveQuery`:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// Fork a fiber that processes the stream
|
|
167
|
+
const fiber = Effect.runFork(
|
|
168
|
+
stream.pipe(
|
|
169
|
+
Stream.runForEach((records) =>
|
|
170
|
+
Effect.sync(() => {
|
|
171
|
+
this.items = records;
|
|
172
|
+
}),
|
|
173
|
+
),
|
|
174
|
+
),
|
|
175
|
+
);
|
|
176
|
+
// Store fiber reference for cleanup
|
|
177
|
+
this.#cleanup = () => Effect.runFork(Fiber.interrupt(fiber));
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Collection Auto-Watch Lifecycle
|
|
181
|
+
|
|
182
|
+
- Watch stream starts in the constructor (items immediately populated)
|
|
183
|
+
- Fiber is stored for cleanup
|
|
184
|
+
- `Database.close()` interrupts all collection fibers
|
|
185
|
+
|
|
186
|
+
### LiveQuery Auto-Dispose
|
|
187
|
+
|
|
188
|
+
LiveQuery uses `$effect` cleanup in Svelte 5. When the component unmounts, `$effect` teardown interrupts the stream fiber. Since LiveQuery is created inside component `<script>`, Svelte tracks it.
|
|
189
|
+
|
|
190
|
+
However, `$effect` cleanup only works if the LiveQuery is created inside a tracking context. For safety, LiveQuery also exposes a `.destroy()` method for manual cleanup, and Database.close() cleans up all active queries.
|
|
191
|
+
|
|
192
|
+
### Error Handling
|
|
193
|
+
|
|
194
|
+
CRUD methods catch Effect failures and:
|
|
195
|
+
|
|
196
|
+
1. Set `collection.error = extractedError`
|
|
197
|
+
2. Re-throw as a plain Error (so `await` callers can also catch)
|
|
198
|
+
|
|
199
|
+
This gives both template-friendly `{#if col.error}` and traditional `try/catch`.
|
|
200
|
+
|
|
201
|
+
### Sync Status
|
|
202
|
+
|
|
203
|
+
Database polls `getSyncStatus()` periodically or listens to sync events to update `database.status` and propagate to collections.
|
|
204
|
+
|
|
205
|
+
## Files to Modify
|
|
206
|
+
|
|
207
|
+
### `src/index.ts`
|
|
208
|
+
|
|
209
|
+
Add svelte re-export:
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
// Svelte bindings
|
|
213
|
+
export * from "./svelte/index.svelte.ts";
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Actually — better to keep it as a separate import path. Users import from `localstr/svelte` or `./svelte/index.svelte.ts`. No changes to `src/index.ts` needed.
|
|
217
|
+
|
|
218
|
+
## Implementation Order
|
|
219
|
+
|
|
220
|
+
1. `src/svelte/live-query.svelte.ts` — LiveQuery class (standalone, testable)
|
|
221
|
+
2. `src/svelte/query.svelte.ts` — SvelteWhereClause/SvelteQueryBuilder/SvelteOrderByBuilder wrappers
|
|
222
|
+
3. `src/svelte/collection.svelte.ts` — Collection class with auto-watch + CRUD
|
|
223
|
+
4. `src/svelte/database.svelte.ts` — Database class with scope management
|
|
224
|
+
5. `src/svelte/index.svelte.ts` — Entry point with createLocalstr wrapper + re-exports
|
|
225
|
+
6. Update `package.json` exports map if needed for `localstr/svelte` path
|
|
226
|
+
|
|
227
|
+
## Verification
|
|
228
|
+
|
|
229
|
+
1. TypeScript compilation: `bun run tsc --noEmit` (ensure .svelte.ts files compile)
|
|
230
|
+
2. Create a demo Svelte component using the new API
|
|
231
|
+
3. Test that Effect scope is properly managed (no leaked fibers)
|
|
232
|
+
4. Test LiveQuery cleanup on destroy
|
|
233
|
+
5. Test error propagation to $state
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release:
|
|
12
|
+
name: Release
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
permissions:
|
|
15
|
+
contents: write
|
|
16
|
+
pull-requests: write
|
|
17
|
+
id-token: write
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Setup Bun
|
|
23
|
+
uses: oven-sh/setup-bun@v2
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: bun install --frozen-lockfile
|
|
27
|
+
|
|
28
|
+
- name: Create Release Pull Request or Publish
|
|
29
|
+
id: changesets
|
|
30
|
+
uses: changesets/action@v1
|
|
31
|
+
with:
|
|
32
|
+
version: bun run version
|
|
33
|
+
publish: bun run release
|
|
34
|
+
env:
|
|
35
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
36
|
+
NPM_CONFIG_PROVENANCE: true
|
package/.oxlintrc.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# localstr
|