@chipmobilesdk/rn-local-db 0.2.0 → 0.3.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +191 -102
  2. package/README.md +728 -728
  3. package/package.json +62 -62
  4. package/skills/sdk-rn-local-db/SKILL.md +0 -1
  5. package/src/adapter/budgets.ts +67 -67
  6. package/src/adapter/collection.ts +303 -298
  7. package/src/adapter/deletion.ts +252 -252
  8. package/src/adapter/index.ts +64 -68
  9. package/src/adapter/lifecycle.ts +517 -520
  10. package/src/adapter/transaction.ts +71 -71
  11. package/src/artifacts/availability.ts +67 -67
  12. package/src/artifacts/cascade.ts +114 -114
  13. package/src/artifacts/paths.ts +29 -29
  14. package/src/artifacts/reconcile.ts +63 -63
  15. package/src/artifacts/store.ts +334 -345
  16. package/src/contract/adapter.ts +76 -78
  17. package/src/contract/attachment.ts +75 -78
  18. package/src/contract/backup.ts +28 -28
  19. package/src/contract/config.ts +54 -54
  20. package/src/contract/encryption.ts +34 -34
  21. package/src/contract/errors.ts +76 -76
  22. package/src/contract/migration.ts +42 -42
  23. package/src/contract/query.ts +81 -81
  24. package/src/contract/record.ts +46 -51
  25. package/src/contract/schema.ts +36 -36
  26. package/src/contract/scope.ts +36 -36
  27. package/src/contract/transaction.ts +52 -54
  28. package/src/crypto/chunked-cipher.ts +153 -150
  29. package/src/crypto/derive.ts +64 -64
  30. package/src/crypto/key-provider.ts +78 -78
  31. package/src/crypto/provider.ts +75 -81
  32. package/src/diagnostics/dev-log.ts +132 -108
  33. package/src/diagnostics/errors.ts +63 -66
  34. package/src/diagnostics/faults.ts +58 -64
  35. package/src/diagnostics/map-native.ts +108 -106
  36. package/src/diagnostics/redact.ts +119 -141
  37. package/src/index.ts +80 -110
  38. package/src/migration/executor.ts +132 -136
  39. package/src/migration/ledger.ts +95 -97
  40. package/src/migration/validate-chain.ts +65 -65
  41. package/src/platform/backup-android-verify.ts +88 -88
  42. package/src/platform/backup-ios.ts +49 -48
  43. package/src/platform/bytes.ts +157 -144
  44. package/src/platform/fs.ts +90 -89
  45. package/src/platform/location.ts +135 -135
  46. package/src/query/budgets.ts +54 -54
  47. package/src/query/compare.ts +91 -91
  48. package/src/query/cursor.ts +100 -100
  49. package/src/query/filter.ts +82 -82
  50. package/src/query/paginate.ts +51 -51
  51. package/src/query/sort.ts +55 -56
  52. package/src/query/validate.ts +171 -171
  53. package/src/schema/declare.ts +42 -45
  54. package/src/schema/validate-config.ts +158 -158
  55. package/src/scope/hash.ts +131 -134
  56. package/src/scope/normalize.ts +83 -90
  57. package/src/scope/validate.ts +83 -83
  58. package/src/sqlite/adapter.ts +26 -26
  59. package/src/sqlite/batch.ts +242 -242
  60. package/src/sqlite/catalog.ts +105 -108
  61. package/src/sqlite/connection.ts +227 -227
  62. package/src/sqlite/index-coverage.ts +54 -56
  63. package/src/sqlite/native.ts +84 -83
  64. package/src/sqlite/row-codec.ts +217 -221
  65. package/src/sqlite/sql-builder.ts +244 -244
  66. package/src/sqlite/transaction-queue.ts +87 -87
  67. package/src/sqlite/transactions.ts +33 -33
  68. package/src/sqlite/values.ts +24 -24
  69. package/src/testing/conformance/attachments.ts +139 -136
  70. package/src/testing/conformance/encryption.ts +151 -150
  71. package/src/testing/conformance/index.ts +161 -161
  72. package/src/testing/conformance/lifecycle.ts +99 -99
  73. package/src/testing/conformance/migrations.ts +177 -177
  74. package/src/testing/conformance/queries.ts +204 -167
  75. package/src/testing/conformance/records.ts +169 -169
  76. package/src/testing/conformance/scopes.ts +86 -88
  77. package/src/testing/conformance/transactions.ts +149 -149
  78. package/src/testing/faults.ts +21 -21
  79. package/src/testing/index.ts +17 -21
  80. package/src/testing/memory-adapter/collection.ts +214 -219
  81. package/src/testing/memory-adapter/index.ts +511 -492
  82. package/src/testing/memory-adapter/migrations.ts +117 -119
  83. package/src/testing/memory-adapter/reset.ts +50 -50
  84. package/src/testing/memory-adapter/store.ts +128 -134
@@ -1,169 +1,169 @@
1
- import { DiagnosticCodes } from '../../contract/errors';
2
- import { isCode } from '../../diagnostics/errors';
3
- import type { ConformanceContext } from './index';
4
-
5
- /** US1: durable CRUD, and the four value conditions staying distinguishable. */
6
- export function registerRecordConformance(context: ConformanceContext): void {
7
- describe('conformance: records', () => {
8
- beforeEach(() => context.reset());
9
-
10
- const scope = { kind: 'guest' } as const;
11
-
12
- it('returns exactly what was stored', async () => {
13
- const handle = await context.adapter.open(context.configFor(scope));
14
- const notes = handle.collection('notes');
15
- const data = {
16
- title: 'alpha',
17
- score: 1.5,
18
- nested: { deep: [1, 'two', null, { three: true }] },
19
- explicitNull: null,
20
- };
21
- await notes.insert({ id: 'a', data });
22
-
23
- expect((await notes.get('a')).data).toEqual(data);
24
- await handle.close();
25
- });
26
-
27
- it('does not mutate the object the caller passed in', async () => {
28
- const handle = await context.adapter.open(context.configFor(scope));
29
- const data = { title: 'alpha' };
30
- await handle.collection('notes').insert({ id: 'a', data });
31
- expect(data).toEqual({ title: 'alpha' });
32
- await handle.close();
33
- });
34
-
35
- it('does not hand back a live reference into the store', async () => {
36
- const handle = await context.adapter.open(context.configFor(scope));
37
- const notes = handle.collection('notes');
38
- await notes.insert({ id: 'a', data: { title: 'alpha' } });
39
-
40
- const first = await notes.get('a');
41
- (first.data as { title: string }).title = 'mutated';
42
-
43
- expect((await notes.get('a')).data).toEqual({ title: 'alpha' });
44
- await handle.close();
45
- });
46
-
47
- it('distinguishes absent record, explicit null, and absent field', async () => {
48
- const handle = await context.adapter.open(context.configFor(scope));
49
- const notes = handle.collection('notes');
50
- await notes.insert({ id: 'null-field', data: { title: null } });
51
- await notes.insert({ id: 'absent-field', data: {} });
52
-
53
- const withNull = await notes.get('null-field');
54
- expect('title' in withNull.data).toBe(true);
55
- expect(withNull.data.title).toBeNull();
56
-
57
- const withAbsent = await notes.get('absent-field');
58
- expect('title' in withAbsent.data).toBe(false);
59
-
60
- expect(await notes.find('never-existed')).toBeNull();
61
- await handle.close();
62
- });
63
-
64
- it('reports a conflict rather than overwriting on insert', async () => {
65
- const handle = await context.adapter.open(context.configFor(scope));
66
- const notes = handle.collection('notes');
67
- await notes.insert({ id: 'a', data: { title: 'first' } });
68
-
69
- let thrown: unknown;
70
- try {
71
- await notes.insert({ id: 'a', data: { title: 'second' } });
72
- } catch (error) {
73
- thrown = error;
74
- }
75
- expect(isCode(thrown, DiagnosticCodes.RECORD_CONFLICT)).toBe(true);
76
- expect((await notes.get('a')).data.title).toBe('first');
77
- await handle.close();
78
- });
79
-
80
- it('reports whether an upsert created or updated', async () => {
81
- const handle = await context.adapter.open(context.configFor(scope));
82
- const notes = handle.collection('notes');
83
- expect((await notes.upsert({ id: 'a', data: { title: 'x' } })).operation).toBe('created');
84
- expect((await notes.upsert({ id: 'a', data: { title: 'y' } })).operation).toBe('updated');
85
- await handle.close();
86
- });
87
-
88
- it('reports record-not-found rather than creating on update', async () => {
89
- const handle = await context.adapter.open(context.configFor(scope));
90
- let thrown: unknown;
91
- try {
92
- await handle.collection('notes').update('ghost', { data: { title: 'x' } });
93
- } catch (error) {
94
- thrown = error;
95
- }
96
- expect(isCode(thrown, DiagnosticCodes.RECORD_NOT_FOUND)).toBe(true);
97
- expect(await handle.collection('notes').find('ghost')).toBeNull();
98
- await handle.close();
99
- });
100
-
101
- it('makes a hard-deleted record not-found', async () => {
102
- const handle = await context.adapter.open(context.configFor(scope));
103
- const notes = handle.collection('notes');
104
- await notes.insert({ id: 'a', data: { title: 'x' } });
105
- await notes.delete('a');
106
- expect(await notes.find('a')).toBeNull();
107
- await handle.close();
108
- });
109
-
110
- it('hides a soft-deleted record by default and restores it', async () => {
111
- const handle = await context.adapter.open(context.configFor(scope));
112
- const notes = handle.collection('notes');
113
- await notes.insert({ id: 'a', data: { title: 'x' } });
114
- await notes.softDelete('a');
115
-
116
- expect(await notes.find('a')).toBeNull();
117
- expect((await notes.list()).records).toHaveLength(0);
118
- expect((await notes.list({ includeSoftDeleted: true })).records).toHaveLength(1);
119
-
120
- await notes.restore('a');
121
- expect(await notes.find('a')).not.toBeNull();
122
- await handle.close();
123
- });
124
-
125
- it('rejects a record over the size budget rather than truncating', async () => {
126
- const handle = await context.adapter.open(context.configFor(scope));
127
- let thrown: unknown;
128
- try {
129
- await handle
130
- .collection('notes')
131
- .insert({ id: 'big', data: { title: 'x'.repeat(300_000) } });
132
- } catch (error) {
133
- thrown = error;
134
- }
135
- expect(isCode(thrown, DiagnosticCodes.RECORD_TOO_LARGE)).toBe(true);
136
- expect(await handle.collection('notes').find('big')).toBeNull();
137
- await handle.close();
138
- });
139
-
140
- it('rejects operations on a closed handle', async () => {
141
- const handle = await context.adapter.open(context.configFor(scope));
142
- await handle.close();
143
-
144
- let thrown: unknown;
145
- try {
146
- await handle.collection('notes').find('a');
147
- } catch (error) {
148
- thrown = error;
149
- }
150
- expect(isCode(thrown, DiagnosticCodes.CLOSED_HANDLE)).toBe(true);
151
- });
152
-
153
- it('persists across a close and reopen', async () => {
154
- const first = await context.adapter.open(context.configFor(scope));
155
- await first.collection('notes').insert({ id: 'a', data: { title: 'kept' } });
156
- await first.close();
157
-
158
- const second = await context.adapter.open(context.configFor(scope));
159
- expect((await second.collection('notes').get('a')).data.title).toBe('kept');
160
- await second.close();
161
- });
162
-
163
- it('rejects a collection that was never declared', async () => {
164
- const handle = await context.adapter.open(context.configFor(scope));
165
- expect(() => handle.collection('nope')).toThrow();
166
- await handle.close();
167
- });
168
- });
169
- }
1
+ import { DiagnosticCodes } from '../../contract/errors';
2
+ import { isCode } from '../../diagnostics/errors';
3
+ import type { ConformanceContext } from './index';
4
+
5
+ /** US1: durable CRUD, and the four value conditions staying distinguishable. */
6
+ export function registerRecordConformance(context: ConformanceContext): void {
7
+ describe('conformance: records', () => {
8
+ beforeEach(() => context.reset());
9
+
10
+ const scope = { kind: 'guest' } as const;
11
+
12
+ it('returns exactly what was stored', async () => {
13
+ const handle = await context.adapter.open(context.configFor(scope));
14
+ const notes = handle.collection('notes');
15
+ const data = {
16
+ title: 'alpha',
17
+ score: 1.5,
18
+ nested: { deep: [1, 'two', null, { three: true }] },
19
+ explicitNull: null,
20
+ };
21
+ await notes.insert({ id: 'a', data });
22
+
23
+ expect((await notes.get('a')).data).toEqual(data);
24
+ await handle.close();
25
+ });
26
+
27
+ it('does not mutate the object the caller passed in', async () => {
28
+ const handle = await context.adapter.open(context.configFor(scope));
29
+ const data = { title: 'alpha' };
30
+ await handle.collection('notes').insert({ id: 'a', data });
31
+ expect(data).toEqual({ title: 'alpha' });
32
+ await handle.close();
33
+ });
34
+
35
+ it('does not hand back a live reference into the store', async () => {
36
+ const handle = await context.adapter.open(context.configFor(scope));
37
+ const notes = handle.collection('notes');
38
+ await notes.insert({ id: 'a', data: { title: 'alpha' } });
39
+
40
+ const first = await notes.get('a');
41
+ (first.data as { title: string }).title = 'mutated';
42
+
43
+ expect((await notes.get('a')).data).toEqual({ title: 'alpha' });
44
+ await handle.close();
45
+ });
46
+
47
+ it('distinguishes absent record, explicit null, and absent field', async () => {
48
+ const handle = await context.adapter.open(context.configFor(scope));
49
+ const notes = handle.collection('notes');
50
+ await notes.insert({ id: 'null-field', data: { title: null } });
51
+ await notes.insert({ id: 'absent-field', data: {} });
52
+
53
+ const withNull = await notes.get('null-field');
54
+ expect('title' in withNull.data).toBe(true);
55
+ expect(withNull.data.title).toBeNull();
56
+
57
+ const withAbsent = await notes.get('absent-field');
58
+ expect('title' in withAbsent.data).toBe(false);
59
+
60
+ expect(await notes.find('never-existed')).toBeNull();
61
+ await handle.close();
62
+ });
63
+
64
+ it('reports a conflict rather than overwriting on insert', async () => {
65
+ const handle = await context.adapter.open(context.configFor(scope));
66
+ const notes = handle.collection('notes');
67
+ await notes.insert({ id: 'a', data: { title: 'first' } });
68
+
69
+ let thrown: unknown;
70
+ try {
71
+ await notes.insert({ id: 'a', data: { title: 'second' } });
72
+ } catch (error) {
73
+ thrown = error;
74
+ }
75
+ expect(isCode(thrown, DiagnosticCodes.RECORD_CONFLICT)).toBe(true);
76
+ expect((await notes.get('a')).data.title).toBe('first');
77
+ await handle.close();
78
+ });
79
+
80
+ it('reports whether an upsert created or updated', async () => {
81
+ const handle = await context.adapter.open(context.configFor(scope));
82
+ const notes = handle.collection('notes');
83
+ expect((await notes.upsert({ id: 'a', data: { title: 'x' } })).operation).toBe('created');
84
+ expect((await notes.upsert({ id: 'a', data: { title: 'y' } })).operation).toBe('updated');
85
+ await handle.close();
86
+ });
87
+
88
+ it('reports record-not-found rather than creating on update', async () => {
89
+ const handle = await context.adapter.open(context.configFor(scope));
90
+ let thrown: unknown;
91
+ try {
92
+ await handle.collection('notes').update('ghost', { data: { title: 'x' } });
93
+ } catch (error) {
94
+ thrown = error;
95
+ }
96
+ expect(isCode(thrown, DiagnosticCodes.RECORD_NOT_FOUND)).toBe(true);
97
+ expect(await handle.collection('notes').find('ghost')).toBeNull();
98
+ await handle.close();
99
+ });
100
+
101
+ it('makes a hard-deleted record not-found', async () => {
102
+ const handle = await context.adapter.open(context.configFor(scope));
103
+ const notes = handle.collection('notes');
104
+ await notes.insert({ id: 'a', data: { title: 'x' } });
105
+ await notes.delete('a');
106
+ expect(await notes.find('a')).toBeNull();
107
+ await handle.close();
108
+ });
109
+
110
+ it('hides a soft-deleted record by default and restores it', async () => {
111
+ const handle = await context.adapter.open(context.configFor(scope));
112
+ const notes = handle.collection('notes');
113
+ await notes.insert({ id: 'a', data: { title: 'x' } });
114
+ await notes.softDelete('a');
115
+
116
+ expect(await notes.find('a')).toBeNull();
117
+ expect((await notes.list()).records).toHaveLength(0);
118
+ expect((await notes.list({ includeSoftDeleted: true })).records).toHaveLength(1);
119
+
120
+ await notes.restore('a');
121
+ expect(await notes.find('a')).not.toBeNull();
122
+ await handle.close();
123
+ });
124
+
125
+ it('rejects a record over the size budget rather than truncating', async () => {
126
+ const handle = await context.adapter.open(context.configFor(scope));
127
+ let thrown: unknown;
128
+ try {
129
+ await handle
130
+ .collection('notes')
131
+ .insert({ id: 'big', data: { title: 'x'.repeat(300_000) } });
132
+ } catch (error) {
133
+ thrown = error;
134
+ }
135
+ expect(isCode(thrown, DiagnosticCodes.RECORD_TOO_LARGE)).toBe(true);
136
+ expect(await handle.collection('notes').find('big')).toBeNull();
137
+ await handle.close();
138
+ });
139
+
140
+ it('rejects operations on a closed handle', async () => {
141
+ const handle = await context.adapter.open(context.configFor(scope));
142
+ await handle.close();
143
+
144
+ let thrown: unknown;
145
+ try {
146
+ await handle.collection('notes').find('a');
147
+ } catch (error) {
148
+ thrown = error;
149
+ }
150
+ expect(isCode(thrown, DiagnosticCodes.CLOSED_HANDLE)).toBe(true);
151
+ });
152
+
153
+ it('persists across a close and reopen', async () => {
154
+ const first = await context.adapter.open(context.configFor(scope));
155
+ await first.collection('notes').insert({ id: 'a', data: { title: 'kept' } });
156
+ await first.close();
157
+
158
+ const second = await context.adapter.open(context.configFor(scope));
159
+ expect((await second.collection('notes').get('a')).data.title).toBe('kept');
160
+ await second.close();
161
+ });
162
+
163
+ it('rejects a collection that was never declared', async () => {
164
+ const handle = await context.adapter.open(context.configFor(scope));
165
+ expect(() => handle.collection('nope')).toThrow();
166
+ await handle.close();
167
+ });
168
+ });
169
+ }
@@ -1,88 +1,86 @@
1
- import type { ScopeDescriptor } from '../../contract/scope';
2
- import type { ConformanceContext } from './index';
3
-
4
- /** US2: scope isolation, proven with unfiltered reads and colliding ids. */
5
- export function registerScopeConformance(context: ConformanceContext): void {
6
- describe('conformance: scopes', () => {
7
- beforeEach(() => context.reset());
8
-
9
- const guest: ScopeDescriptor = { kind: 'guest' };
10
- const alice: ScopeDescriptor = { kind: 'identity', id: 'alice' };
11
- const bob: ScopeDescriptor = { kind: 'identity', id: 'bob' };
12
-
13
- it('returns nothing from another scope, even unfiltered', async () => {
14
- const a = await context.adapter.open(context.configFor(alice));
15
- await a.collection('notes').insert({ id: 'shared-id', data: { title: 'alice' } });
16
- await a.close();
17
-
18
- const b = await context.adapter.open(context.configFor(bob));
19
- expect((await b.collection('notes').list()).records).toHaveLength(0);
20
- expect(await b.collection('notes').count()).toBe(0);
21
- expect(await b.collection('notes').find('shared-id')).toBeNull();
22
- await b.close();
23
- });
24
-
25
- it('lets the same id exist independently in two scopes', async () => {
26
- const a = await context.adapter.open(context.configFor(alice));
27
- const b = await context.adapter.open(context.configFor(bob));
28
-
29
- await a.collection('notes').insert({ id: 'same', data: { title: 'alice' } });
30
- await b.collection('notes').insert({ id: 'same', data: { title: 'bob' } });
31
-
32
- expect((await a.collection('notes').get('same')).data.title).toBe('alice');
33
- expect((await b.collection('notes').get('same')).data.title).toBe('bob');
34
- await a.close();
35
- await b.close();
36
- });
37
-
38
- it('keeps three concurrently open handles independent', async () => {
39
- const handles = await Promise.all(
40
- [guest, alice, bob].map(scope => context.adapter.open(context.configFor(scope))),
41
- );
42
-
43
- await Promise.all(
44
- handles.map((handle, index) =>
45
- handle.collection('notes').insert({ id: 'x', data: { score: index } }),
46
- ),
47
- );
48
-
49
- for (const [index, handle] of handles.entries()) {
50
- expect((await handle.collection('notes').get('x')).data.score).toBe(index);
51
- expect(await handle.collection('notes').count()).toBe(1);
52
- }
53
- await Promise.all(handles.map(handle => handle.close()));
54
- });
55
-
56
- it('exposes the scope digest rather than the components', async () => {
57
- const handle = await context.adapter.open(context.configFor(alice));
58
- expect(handle.scopeKey).toMatch(/^[0-9a-f]{64}$/);
59
- expect(handle.scopeKey).not.toContain('alice');
60
- await handle.close();
61
- });
62
-
63
- it('resolves the same descriptor to the same scope across opens', async () => {
64
- const first = await context.adapter.open(context.configFor(alice));
65
- const key = first.scopeKey;
66
- await first.close();
67
-
68
- const second = await context.adapter.open(
69
- context.configFor({ kind: 'identity', id: 'alice' }),
70
- );
71
- expect(second.scopeKey).toBe(key);
72
- await second.close();
73
- });
74
-
75
- it('treats a differing component as a different scope', async () => {
76
- const plain = await context.adapter.open(context.configFor(alice));
77
- const tenanted = await context.adapter.open(
78
- context.configFor({ ...alice, tenant: 'acme' }),
79
- );
80
- expect(tenanted.scopeKey).not.toBe(plain.scopeKey);
81
-
82
- await plain.collection('notes').insert({ id: 'x', data: { title: 'plain' } });
83
- expect(await tenanted.collection('notes').find('x')).toBeNull();
84
- await plain.close();
85
- await tenanted.close();
86
- });
87
- });
88
- }
1
+ import type { ScopeDescriptor } from '../../contract/scope';
2
+ import type { ConformanceContext } from './index';
3
+
4
+ /** US2: scope isolation, proven with unfiltered reads and colliding ids. */
5
+ export function registerScopeConformance(context: ConformanceContext): void {
6
+ describe('conformance: scopes', () => {
7
+ beforeEach(() => context.reset());
8
+
9
+ const guest: ScopeDescriptor = { kind: 'guest' };
10
+ const alice: ScopeDescriptor = { kind: 'identity', id: 'alice' };
11
+ const bob: ScopeDescriptor = { kind: 'identity', id: 'bob' };
12
+
13
+ it('returns nothing from another scope, even unfiltered', async () => {
14
+ const a = await context.adapter.open(context.configFor(alice));
15
+ await a.collection('notes').insert({ id: 'shared-id', data: { title: 'alice' } });
16
+ await a.close();
17
+
18
+ const b = await context.adapter.open(context.configFor(bob));
19
+ expect((await b.collection('notes').list()).records).toHaveLength(0);
20
+ expect(await b.collection('notes').count()).toBe(0);
21
+ expect(await b.collection('notes').find('shared-id')).toBeNull();
22
+ await b.close();
23
+ });
24
+
25
+ it('lets the same id exist independently in two scopes', async () => {
26
+ const a = await context.adapter.open(context.configFor(alice));
27
+ const b = await context.adapter.open(context.configFor(bob));
28
+
29
+ await a.collection('notes').insert({ id: 'same', data: { title: 'alice' } });
30
+ await b.collection('notes').insert({ id: 'same', data: { title: 'bob' } });
31
+
32
+ expect((await a.collection('notes').get('same')).data.title).toBe('alice');
33
+ expect((await b.collection('notes').get('same')).data.title).toBe('bob');
34
+ await a.close();
35
+ await b.close();
36
+ });
37
+
38
+ it('keeps three concurrently open handles independent', async () => {
39
+ const handles = await Promise.all(
40
+ [guest, alice, bob].map(scope => context.adapter.open(context.configFor(scope))),
41
+ );
42
+
43
+ await Promise.all(
44
+ handles.map((handle, index) =>
45
+ handle.collection('notes').insert({ id: 'x', data: { score: index } }),
46
+ ),
47
+ );
48
+
49
+ for (const [index, handle] of handles.entries()) {
50
+ expect((await handle.collection('notes').get('x')).data.score).toBe(index);
51
+ expect(await handle.collection('notes').count()).toBe(1);
52
+ }
53
+ await Promise.all(handles.map(handle => handle.close()));
54
+ });
55
+
56
+ it('exposes the scope digest rather than the components', async () => {
57
+ const handle = await context.adapter.open(context.configFor(alice));
58
+ expect(handle.scopeKey).toMatch(/^[0-9a-f]{64}$/);
59
+ expect(handle.scopeKey).not.toContain('alice');
60
+ await handle.close();
61
+ });
62
+
63
+ it('resolves the same descriptor to the same scope across opens', async () => {
64
+ const first = await context.adapter.open(context.configFor(alice));
65
+ const key = first.scopeKey;
66
+ await first.close();
67
+
68
+ const second = await context.adapter.open(
69
+ context.configFor({ kind: 'identity', id: 'alice' }),
70
+ );
71
+ expect(second.scopeKey).toBe(key);
72
+ await second.close();
73
+ });
74
+
75
+ it('treats a differing component as a different scope', async () => {
76
+ const plain = await context.adapter.open(context.configFor(alice));
77
+ const tenanted = await context.adapter.open(context.configFor({ ...alice, tenant: 'acme' }));
78
+ expect(tenanted.scopeKey).not.toBe(plain.scopeKey);
79
+
80
+ await plain.collection('notes').insert({ id: 'x', data: { title: 'plain' } });
81
+ expect(await tenanted.collection('notes').find('x')).toBeNull();
82
+ await plain.close();
83
+ await tenanted.close();
84
+ });
85
+ });
86
+ }