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.
Files changed (75) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.context/attachments/pasted_text_2026-03-07_14-02-40.txt +571 -0
  4. package/.context/attachments/pasted_text_2026-03-07_15-48-27.txt +498 -0
  5. package/.context/notes.md +0 -0
  6. package/.context/plans/add-changesets-to-douala-v4.md +48 -0
  7. package/.context/plans/dexie-js-style-query-language-for-localstr.md +115 -0
  8. package/.context/plans/dexie-js-style-query-language-with-per-collection-.md +336 -0
  9. package/.context/plans/implementation-plan-localstr-v0-2.md +263 -0
  10. package/.context/plans/project-init-effect-v4-bun-oxlint-oxfmt-vitest.md +71 -0
  11. package/.context/plans/revise-localstr-prd-v0-2.md +132 -0
  12. package/.context/plans/svelte-5-runes-bindings-for-localstr.md +233 -0
  13. package/.context/todos.md +0 -0
  14. package/.github/workflows/release.yml +36 -0
  15. package/.oxlintrc.json +8 -0
  16. package/README.md +1 -0
  17. package/bun.lock +705 -0
  18. package/examples/svelte/bun.lock +261 -0
  19. package/examples/svelte/package.json +21 -0
  20. package/examples/svelte/src/app.html +11 -0
  21. package/examples/svelte/src/lib/db.ts +44 -0
  22. package/examples/svelte/src/routes/+page.svelte +322 -0
  23. package/examples/svelte/svelte.config.js +16 -0
  24. package/examples/svelte/tsconfig.json +6 -0
  25. package/examples/svelte/vite.config.ts +6 -0
  26. package/examples/vanilla/app.ts +219 -0
  27. package/examples/vanilla/index.html +144 -0
  28. package/examples/vanilla/serve.ts +42 -0
  29. package/package.json +46 -0
  30. package/prds/localstr-v0.2.md +221 -0
  31. package/prek.toml +10 -0
  32. package/scripts/validate.ts +392 -0
  33. package/src/crud/collection-handle.ts +189 -0
  34. package/src/crud/query-builder.ts +414 -0
  35. package/src/crud/watch.ts +78 -0
  36. package/src/db/create-localstr.ts +217 -0
  37. package/src/db/database-handle.ts +16 -0
  38. package/src/db/identity.ts +49 -0
  39. package/src/errors.ts +37 -0
  40. package/src/index.ts +32 -0
  41. package/src/main.ts +10 -0
  42. package/src/schema/collection.ts +53 -0
  43. package/src/schema/field.ts +25 -0
  44. package/src/schema/types.ts +19 -0
  45. package/src/schema/validate.ts +111 -0
  46. package/src/storage/events-store.ts +24 -0
  47. package/src/storage/giftwraps-store.ts +23 -0
  48. package/src/storage/idb.ts +244 -0
  49. package/src/storage/lww.ts +17 -0
  50. package/src/storage/records-store.ts +76 -0
  51. package/src/svelte/collection.svelte.ts +87 -0
  52. package/src/svelte/database.svelte.ts +83 -0
  53. package/src/svelte/index.svelte.ts +52 -0
  54. package/src/svelte/live-query.svelte.ts +29 -0
  55. package/src/svelte/query.svelte.ts +101 -0
  56. package/src/sync/gift-wrap.ts +33 -0
  57. package/src/sync/negentropy.ts +83 -0
  58. package/src/sync/publish-queue.ts +61 -0
  59. package/src/sync/relay.ts +239 -0
  60. package/src/sync/sync-service.ts +183 -0
  61. package/src/sync/sync-status.ts +17 -0
  62. package/src/utils/uuid.ts +22 -0
  63. package/src/vendor/negentropy.js +616 -0
  64. package/tests/db/create-localstr.test.ts +174 -0
  65. package/tests/db/identity.test.ts +33 -0
  66. package/tests/main.test.ts +9 -0
  67. package/tests/schema/collection.test.ts +27 -0
  68. package/tests/schema/field.test.ts +41 -0
  69. package/tests/schema/validate.test.ts +85 -0
  70. package/tests/setup.ts +1 -0
  71. package/tests/storage/idb.test.ts +144 -0
  72. package/tests/storage/lww.test.ts +33 -0
  73. package/tests/sync/gift-wrap.test.ts +56 -0
  74. package/tsconfig.json +18 -0
  75. package/vitest.config.ts +8 -0
@@ -0,0 +1,174 @@
1
+ import { describe, expect } from "vitest";
2
+ import { it } from "@effect/vitest";
3
+ import { Effect } from "effect";
4
+ import { field } from "../../src/schema/field.ts";
5
+ import { collection } from "../../src/schema/collection.ts";
6
+ import { createLocalstr } from "../../src/db/create-localstr.ts";
7
+
8
+ describe("createLocalstr", () => {
9
+ it.effect("creates a database and performs CRUD", () =>
10
+ Effect.gen(function* () {
11
+ const todos = collection("todos", {
12
+ title: field.string(),
13
+ done: field.boolean(),
14
+ });
15
+
16
+ const db = yield* createLocalstr({
17
+ schema: { todos },
18
+ relays: ["wss://relay.example.com"],
19
+ dbName: "test-crud",
20
+ });
21
+
22
+ const col = db.collection("todos");
23
+
24
+ // Add
25
+ const id = yield* col.add({
26
+ title: "Buy milk",
27
+ done: false,
28
+ } as any);
29
+ expect(id).toBeDefined();
30
+ expect(id.length).toBeGreaterThan(0);
31
+
32
+ // Get
33
+ const record = yield* col.get(id);
34
+ expect(record.id).toBe(id);
35
+ expect(record.title).toBe("Buy milk");
36
+ expect(record.done).toBe(false);
37
+
38
+ // Update
39
+ yield* col.update(id, { done: true } as any);
40
+ const updated = yield* col.get(id);
41
+ expect(updated.done).toBe(true);
42
+ expect(updated.title).toBe("Buy milk");
43
+
44
+ // Count
45
+ const count = yield* col.count();
46
+ expect(count).toBe(1);
47
+
48
+ // First
49
+ const first = yield* col.first();
50
+ expect(first).not.toBeNull();
51
+ expect(first!.id).toBe(id);
52
+
53
+ // Delete
54
+ yield* col.delete(id);
55
+ const countAfter = yield* col.count();
56
+ expect(countAfter).toBe(0);
57
+
58
+ // Get after delete returns NotFoundError
59
+ const getResult = yield* Effect.result(col.get(id));
60
+ expect(getResult._tag).toBe("Failure");
61
+ }),
62
+ );
63
+
64
+ it.effect("rejects missing relays", () =>
65
+ Effect.gen(function* () {
66
+ const todos = collection("todos", {
67
+ title: field.string(),
68
+ });
69
+ const result = yield* Effect.result(
70
+ createLocalstr({
71
+ schema: { todos },
72
+ relays: [],
73
+ dbName: "test-no-relays",
74
+ }),
75
+ );
76
+ expect(result._tag).toBe("Failure");
77
+ }),
78
+ );
79
+
80
+ it.effect("exports key", () =>
81
+ Effect.gen(function* () {
82
+ const todos = collection("todos", {
83
+ title: field.string(),
84
+ });
85
+ const db = yield* createLocalstr({
86
+ schema: { todos },
87
+ relays: ["wss://relay.example.com"],
88
+ dbName: "test-key",
89
+ });
90
+ const key = db.exportKey();
91
+ expect(key).toBeDefined();
92
+ expect(key.length).toBe(64);
93
+ }),
94
+ );
95
+
96
+ it.effect("rebuild regenerates records from events", () =>
97
+ Effect.gen(function* () {
98
+ const todos = collection("todos", {
99
+ title: field.string(),
100
+ done: field.boolean(),
101
+ });
102
+
103
+ const db = yield* createLocalstr({
104
+ schema: { todos },
105
+ relays: ["wss://relay.example.com"],
106
+ dbName: "test-rebuild",
107
+ });
108
+
109
+ const col = db.collection("todos");
110
+ const id = yield* col.add({
111
+ title: "Rebuild test",
112
+ done: false,
113
+ } as any);
114
+
115
+ yield* col.update(id, { done: true } as any);
116
+
117
+ yield* db.rebuild();
118
+
119
+ const record = yield* col.get(id);
120
+ expect(record.title).toBe("Rebuild test");
121
+ expect(record.done).toBe(true);
122
+ }),
123
+ );
124
+
125
+ it.effect("getSyncStatus returns idle", () =>
126
+ Effect.gen(function* () {
127
+ const todos = collection("todos", {
128
+ title: field.string(),
129
+ });
130
+ const db = yield* createLocalstr({
131
+ schema: { todos },
132
+ relays: ["wss://relay.example.com"],
133
+ dbName: "test-status",
134
+ });
135
+ const status = yield* db.getSyncStatus();
136
+ expect(status).toBe("idle");
137
+ }),
138
+ );
139
+
140
+ it.effect("where().equals() filters records", () =>
141
+ Effect.gen(function* () {
142
+ const todos = collection("todos", {
143
+ title: field.string(),
144
+ done: field.boolean(),
145
+ });
146
+
147
+ const db = yield* createLocalstr({
148
+ schema: { todos },
149
+ relays: ["wss://relay.example.com"],
150
+ dbName: "test-where",
151
+ });
152
+
153
+ const col = db.collection("todos");
154
+ yield* col.add({ title: "A", done: false } as any);
155
+ yield* col.add({ title: "B", done: true } as any);
156
+ yield* col.add({ title: "C", done: false } as any);
157
+
158
+ const where = col.where("done");
159
+ const doneItems = yield* where.equals(true).get();
160
+ expect(doneItems.length).toBe(1);
161
+ expect(doneItems[0]!.title).toBe("B");
162
+
163
+ const notDoneItems = yield* where.equals(false).get();
164
+ expect(notDoneItems.length).toBe(2);
165
+
166
+ const count = yield* where.equals(true).count();
167
+ expect(count).toBe(1);
168
+
169
+ const first = yield* where.equals(true).first();
170
+ expect(first).not.toBeNull();
171
+ expect(first!.title).toBe("B");
172
+ }),
173
+ );
174
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, expect } from "vitest";
2
+ import { it } from "@effect/vitest";
3
+ import { Effect } from "effect";
4
+ import { createIdentity } from "../../src/db/identity.ts";
5
+
6
+ describe("identity", () => {
7
+ it.effect("generates a random key", () =>
8
+ Effect.gen(function* () {
9
+ const identity = yield* createIdentity();
10
+ expect(identity.privateKey.length).toBe(32);
11
+ expect(identity.exportKey().length).toBe(64);
12
+ }),
13
+ );
14
+
15
+ it.effect("accepts a supplied key", () =>
16
+ Effect.gen(function* () {
17
+ const key = new Uint8Array(32);
18
+ key.fill(1);
19
+ const identity = yield* createIdentity(key);
20
+ expect(identity.privateKey).toBe(key);
21
+ expect(identity.exportKey()).toBe(
22
+ "0101010101010101010101010101010101010101010101010101010101010101",
23
+ );
24
+ }),
25
+ );
26
+
27
+ it.effect("rejects invalid key length", () =>
28
+ Effect.gen(function* () {
29
+ const result = yield* Effect.result(createIdentity(new Uint8Array(16)));
30
+ expect(result._tag).toBe("Failure");
31
+ }),
32
+ );
33
+ });
@@ -0,0 +1,9 @@
1
+ import { it, expect } from "@effect/vitest";
2
+ import { Effect } from "effect";
3
+
4
+ it.effect("should return 42", () =>
5
+ Effect.gen(function* () {
6
+ const result = yield* Effect.succeed(42);
7
+ expect(result).toBe(42);
8
+ }),
9
+ );
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { field } from "../../src/schema/field.ts";
3
+ import { collection } from "../../src/schema/collection.ts";
4
+
5
+ describe("collection builder", () => {
6
+ it("creates a valid collection", () => {
7
+ const col = collection("todos", {
8
+ title: field.string(),
9
+ done: field.boolean(),
10
+ });
11
+ expect(col._tag).toBe("CollectionDef");
12
+ expect(col.name).toBe("todos");
13
+ expect(Object.keys(col.fields)).toEqual(["title", "done"]);
14
+ });
15
+
16
+ it("rejects empty name", () => {
17
+ expect(() => collection("", { title: field.string() })).toThrow();
18
+ });
19
+
20
+ it("rejects empty fields", () => {
21
+ expect(() => collection("empty", {})).toThrow();
22
+ });
23
+
24
+ it("rejects reserved field names", () => {
25
+ expect(() => collection("bad", { id: field.string() })).toThrow();
26
+ });
27
+ });
@@ -0,0 +1,41 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { field } from "../../src/schema/field.ts";
3
+
4
+ describe("field builders", () => {
5
+ test("field.string() creates a string field", () => {
6
+ const f = field.string();
7
+ expect(f._tag).toBe("FieldDef");
8
+ expect(f.kind).toBe("string");
9
+ expect(f.isOptional).toBe(false);
10
+ expect(f.isArray).toBe(false);
11
+ });
12
+
13
+ test("field.number() creates a number field", () => {
14
+ const f = field.number();
15
+ expect(f.kind).toBe("number");
16
+ });
17
+
18
+ test("field.boolean() creates a boolean field", () => {
19
+ const f = field.boolean();
20
+ expect(f.kind).toBe("boolean");
21
+ });
22
+
23
+ test("field.json() creates a json field", () => {
24
+ const f = field.json();
25
+ expect(f.kind).toBe("json");
26
+ });
27
+
28
+ test("field.optional() wraps a field as optional", () => {
29
+ const f = field.optional(field.string());
30
+ expect(f.kind).toBe("string");
31
+ expect(f.isOptional).toBe(true);
32
+ expect(f.isArray).toBe(false);
33
+ });
34
+
35
+ test("field.array() wraps a field as array", () => {
36
+ const f = field.array(field.number());
37
+ expect(f.kind).toBe("number");
38
+ expect(f.isOptional).toBe(false);
39
+ expect(f.isArray).toBe(true);
40
+ });
41
+ });
@@ -0,0 +1,85 @@
1
+ import { describe, expect } from "vitest";
2
+ import { it } from "@effect/vitest";
3
+ import { Effect } from "effect";
4
+ import { field } from "../../src/schema/field.ts";
5
+ import { collection } from "../../src/schema/collection.ts";
6
+ import { buildValidator, buildPartialValidator } from "../../src/schema/validate.ts";
7
+
8
+ describe("schema validation", () => {
9
+ it.effect("validates a correct record", () =>
10
+ Effect.gen(function* () {
11
+ const def = collection("todos", {
12
+ title: field.string(),
13
+ done: field.boolean(),
14
+ });
15
+ const validate = buildValidator("todos", def);
16
+ const result = yield* validate({
17
+ id: "abc",
18
+ title: "Test",
19
+ done: false,
20
+ });
21
+ expect(result).toEqual({ id: "abc", title: "Test", done: false });
22
+ }),
23
+ );
24
+
25
+ it.effect("rejects invalid types", () =>
26
+ Effect.gen(function* () {
27
+ const def = collection("todos", {
28
+ title: field.string(),
29
+ });
30
+ const validate = buildValidator("todos", def);
31
+ const result = yield* Effect.result(validate({ id: "abc", title: 42 }));
32
+ expect(result._tag).toBe("Failure");
33
+ }),
34
+ );
35
+
36
+ it.effect("handles optional fields", () =>
37
+ Effect.gen(function* () {
38
+ const def = collection("notes", {
39
+ text: field.string(),
40
+ tag: field.optional(field.string()),
41
+ });
42
+ const validate = buildValidator("notes", def);
43
+ const result = yield* validate({
44
+ id: "abc",
45
+ text: "hello",
46
+ tag: undefined,
47
+ });
48
+ expect(result).toEqual({ id: "abc", text: "hello", tag: undefined });
49
+ }),
50
+ );
51
+
52
+ it.effect("validates array fields", () =>
53
+ Effect.gen(function* () {
54
+ const def = collection("lists", {
55
+ items: field.array(field.string()),
56
+ });
57
+ const validate = buildValidator("lists", def);
58
+ const result = yield* validate({ id: "abc", items: ["a", "b"] });
59
+ expect(result).toEqual({ id: "abc", items: ["a", "b"] });
60
+ }),
61
+ );
62
+
63
+ it.effect("partial validator accepts subset of fields", () =>
64
+ Effect.gen(function* () {
65
+ const def = collection("todos", {
66
+ title: field.string(),
67
+ done: field.boolean(),
68
+ });
69
+ const validate = buildPartialValidator("todos", def);
70
+ const result = yield* validate({ title: "Updated" });
71
+ expect(result).toEqual({ title: "Updated" });
72
+ }),
73
+ );
74
+
75
+ it.effect("partial validator rejects unknown fields", () =>
76
+ Effect.gen(function* () {
77
+ const def = collection("todos", {
78
+ title: field.string(),
79
+ });
80
+ const validate = buildPartialValidator("todos", def);
81
+ const result = yield* Effect.result(validate({ unknown: "value" }));
82
+ expect(result._tag).toBe("Failure");
83
+ }),
84
+ );
85
+ });
package/tests/setup.ts ADDED
@@ -0,0 +1 @@
1
+ import "fake-indexeddb/auto";
@@ -0,0 +1,144 @@
1
+ import { describe, expect } from "vitest";
2
+ import { it } from "@effect/vitest";
3
+ import { Effect } from "effect";
4
+ import { openIDBStorage } from "../../src/storage/idb.ts";
5
+ import { field } from "../../src/schema/field.ts";
6
+ import { collection } from "../../src/schema/collection.ts";
7
+
8
+ const makeSchema = () => ({
9
+ todos: collection("todos", {
10
+ title: field.string(),
11
+ }),
12
+ notes: collection("notes", {
13
+ text: field.string(),
14
+ }),
15
+ });
16
+
17
+ describe("IDBStorage", () => {
18
+ it.effect("opens and closes without error", () =>
19
+ Effect.gen(function* () {
20
+ const schema = makeSchema();
21
+ const storage = yield* openIDBStorage("test-open", schema);
22
+ expect(storage).toBeDefined();
23
+ }),
24
+ );
25
+
26
+ it.effect("puts and gets a record", () =>
27
+ Effect.gen(function* () {
28
+ const schema = makeSchema();
29
+ const storage = yield* openIDBStorage("test-put-get", schema);
30
+ yield* storage.putRecord("todos", {
31
+ id: "r1",
32
+ title: "Test",
33
+ _deleted: false,
34
+ _updatedAt: 100,
35
+ });
36
+ const record = yield* storage.getRecord("todos", "r1");
37
+ expect(record).toBeDefined();
38
+ expect(record!.title).toBe("Test");
39
+ }),
40
+ );
41
+
42
+ it.effect("gets all records for a collection", () =>
43
+ Effect.gen(function* () {
44
+ const schema = makeSchema();
45
+ const storage = yield* openIDBStorage("test-get-all", schema);
46
+ yield* storage.putRecord("todos", {
47
+ id: "r1",
48
+ title: "A",
49
+ _deleted: false,
50
+ _updatedAt: 100,
51
+ });
52
+ yield* storage.putRecord("todos", {
53
+ id: "r2",
54
+ title: "B",
55
+ _deleted: false,
56
+ _updatedAt: 200,
57
+ });
58
+ yield* storage.putRecord("notes", {
59
+ id: "r3",
60
+ text: "C",
61
+ _deleted: false,
62
+ _updatedAt: 300,
63
+ });
64
+ const todos = yield* storage.getAllRecords("todos");
65
+ expect(todos.length).toBe(2);
66
+ const notes = yield* storage.getAllRecords("notes");
67
+ expect(notes.length).toBe(1);
68
+ }),
69
+ );
70
+
71
+ it.effect("clears records store", () =>
72
+ Effect.gen(function* () {
73
+ const schema = makeSchema();
74
+ const storage = yield* openIDBStorage("test-clear", schema);
75
+ yield* storage.putRecord("todos", {
76
+ id: "r1",
77
+ title: "X",
78
+ _deleted: false,
79
+ _updatedAt: 100,
80
+ });
81
+ yield* storage.clearRecords("todos");
82
+ const all = yield* storage.getAllRecords("todos");
83
+ expect(all.length).toBe(0);
84
+ }),
85
+ );
86
+
87
+ it.effect("puts and gets events", () =>
88
+ Effect.gen(function* () {
89
+ const schema = makeSchema();
90
+ const storage = yield* openIDBStorage("test-events", schema);
91
+ yield* storage.putEvent({
92
+ id: "e1",
93
+ collection: "todos",
94
+ recordId: "r1",
95
+ kind: "create",
96
+ data: { title: "Test" },
97
+ createdAt: 100,
98
+ });
99
+ const event = yield* storage.getEvent("e1");
100
+ expect(event).toBeDefined();
101
+ expect(event!.recordId).toBe("r1");
102
+ }),
103
+ );
104
+
105
+ it.effect("gets events by record", () =>
106
+ Effect.gen(function* () {
107
+ const schema = makeSchema();
108
+ const storage = yield* openIDBStorage("test-events-by-record", schema);
109
+ yield* storage.putEvent({
110
+ id: "e1",
111
+ collection: "todos",
112
+ recordId: "r1",
113
+ kind: "create",
114
+ data: { title: "V1" },
115
+ createdAt: 100,
116
+ });
117
+ yield* storage.putEvent({
118
+ id: "e2",
119
+ collection: "todos",
120
+ recordId: "r1",
121
+ kind: "update",
122
+ data: { title: "V2" },
123
+ createdAt: 200,
124
+ });
125
+ const events = yield* storage.getEventsByRecord("todos", "r1");
126
+ expect(events.length).toBe(2);
127
+ }),
128
+ );
129
+
130
+ it.effect("puts and gets gift wraps", () =>
131
+ Effect.gen(function* () {
132
+ const schema = makeSchema();
133
+ const storage = yield* openIDBStorage("test-giftwraps", schema);
134
+ yield* storage.putGiftWrap({
135
+ id: "gw1",
136
+ event: { kind: 1059 },
137
+ createdAt: 100,
138
+ });
139
+ const gw = yield* storage.getGiftWrap("gw1");
140
+ expect(gw).toBeDefined();
141
+ expect(gw!.event.kind).toBe(1059);
142
+ }),
143
+ );
144
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { resolveWinner } from "../../src/storage/lww.ts";
3
+
4
+ describe("LWW resolution", () => {
5
+ test("incoming wins when no existing", () => {
6
+ const incoming = { id: "b", createdAt: 100 };
7
+ expect(resolveWinner(null, incoming)).toBe(incoming);
8
+ });
9
+
10
+ test("newer timestamp wins", () => {
11
+ const existing = { id: "a", createdAt: 100 };
12
+ const incoming = { id: "b", createdAt: 200 };
13
+ expect(resolveWinner(existing, incoming)).toBe(incoming);
14
+ });
15
+
16
+ test("older timestamp loses", () => {
17
+ const existing = { id: "a", createdAt: 200 };
18
+ const incoming = { id: "b", createdAt: 100 };
19
+ expect(resolveWinner(existing, incoming)).toBe(existing);
20
+ });
21
+
22
+ test("tie broken by lowest event ID", () => {
23
+ const existing = { id: "b", createdAt: 100 };
24
+ const incoming = { id: "a", createdAt: 100 };
25
+ expect(resolveWinner(existing, incoming)).toBe(incoming); // "a" < "b"
26
+ });
27
+
28
+ test("tie: existing wins when its ID is lower", () => {
29
+ const existing = { id: "a", createdAt: 100 };
30
+ const incoming = { id: "b", createdAt: 100 };
31
+ expect(resolveWinner(existing, incoming)).toBe(existing); // "a" < "b"
32
+ });
33
+ });
@@ -0,0 +1,56 @@
1
+ import { describe, expect } from "vitest";
2
+ import { it } from "@effect/vitest";
3
+ import { Effect } from "effect";
4
+ import { generateSecretKey, getPublicKey } from "nostr-tools/pure";
5
+ import { createGiftWrapHandle } from "../../src/sync/gift-wrap.ts";
6
+
7
+ describe("gift wrap", () => {
8
+ it.effect("round-trips wrap and unwrap", () =>
9
+ Effect.gen(function* () {
10
+ const sk = generateSecretKey();
11
+ const pk = getPublicKey(sk);
12
+ const handle = createGiftWrapHandle(sk, pk);
13
+
14
+ const giftWrap = yield* handle.wrap({
15
+ kind: 1,
16
+ content: JSON.stringify({ title: "Test", done: false }),
17
+ tags: [["d", "todos:abc-123"]],
18
+ created_at: Math.floor(Date.now() / 1000),
19
+ });
20
+
21
+ expect(giftWrap.kind).toBe(1059);
22
+ expect(giftWrap.id).toBeDefined();
23
+ expect(giftWrap.sig).toBeDefined();
24
+
25
+ const rumor = yield* handle.unwrap(giftWrap);
26
+ expect(rumor.kind).toBe(1);
27
+ expect(rumor.content).toBe(JSON.stringify({ title: "Test", done: false }));
28
+ const dTag = rumor.tags.find((t: string[]) => t[0] === "d");
29
+ expect(dTag).toBeDefined();
30
+ expect(dTag![1]).toBe("todos:abc-123");
31
+ }),
32
+ );
33
+
34
+ it.effect("gift wrap uses random key (different pubkey each time)", () =>
35
+ Effect.gen(function* () {
36
+ const sk = generateSecretKey();
37
+ const pk = getPublicKey(sk);
38
+ const handle = createGiftWrapHandle(sk, pk);
39
+
40
+ const rumor = {
41
+ kind: 1,
42
+ content: "test",
43
+ tags: [],
44
+ created_at: Math.floor(Date.now() / 1000),
45
+ };
46
+
47
+ const gw1 = yield* handle.wrap(rumor);
48
+ const gw2 = yield* handle.wrap(rumor);
49
+
50
+ // Gift wraps should have different pubkeys (random disposable keys)
51
+ expect(gw1.pubkey).not.toBe(gw2.pubkey);
52
+ // And different from the author's pubkey
53
+ expect(gw1.pubkey).not.toBe(pk);
54
+ }),
55
+ );
56
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022", "DOM"],
7
+ "strict": true,
8
+ "exactOptionalPropertyTypes": true,
9
+ "skipLibCheck": true,
10
+ "isolatedModules": true,
11
+ "declaration": true,
12
+ "noEmit": true,
13
+ "esModuleInterop": true,
14
+ "resolveJsonModule": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*.ts", "src/**/*.svelte.ts", "tests/**/*.ts", "scripts/**/*.ts"]
18
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["tests/**/*.test.ts"],
6
+ setupFiles: ["tests/setup.ts"],
7
+ },
8
+ });