@issuegraph/store 0.1.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 (47) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +239 -0
  3. package/dist/adapters/memory.d.ts +30 -0
  4. package/dist/adapters/memory.d.ts.map +1 -0
  5. package/dist/adapters/memory.js +81 -0
  6. package/dist/adapters/memory.js.map +1 -0
  7. package/dist/adapters/scripted.d.ts +67 -0
  8. package/dist/adapters/scripted.d.ts.map +1 -0
  9. package/dist/adapters/scripted.js +89 -0
  10. package/dist/adapters/scripted.js.map +1 -0
  11. package/dist/change.d.ts +75 -0
  12. package/dist/change.d.ts.map +1 -0
  13. package/dist/change.js +79 -0
  14. package/dist/change.js.map +1 -0
  15. package/dist/equality.d.ts +27 -0
  16. package/dist/equality.d.ts.map +1 -0
  17. package/dist/equality.js +60 -0
  18. package/dist/equality.js.map +1 -0
  19. package/dist/index.d.ts +22 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +21 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/model.d.ts +105 -0
  24. package/dist/model.d.ts.map +1 -0
  25. package/dist/model.js +64 -0
  26. package/dist/model.js.map +1 -0
  27. package/dist/mutation.d.ts +83 -0
  28. package/dist/mutation.d.ts.map +1 -0
  29. package/dist/mutation.js +58 -0
  30. package/dist/mutation.js.map +1 -0
  31. package/dist/source.d.ts +139 -0
  32. package/dist/source.d.ts.map +1 -0
  33. package/dist/source.js +17 -0
  34. package/dist/source.js.map +1 -0
  35. package/dist/store.d.ts +120 -0
  36. package/dist/store.d.ts.map +1 -0
  37. package/dist/store.js +529 -0
  38. package/dist/store.js.map +1 -0
  39. package/dist/validity.d.ts +44 -0
  40. package/dist/validity.d.ts.map +1 -0
  41. package/dist/validity.js +125 -0
  42. package/dist/validity.js.map +1 -0
  43. package/dist/write.d.ts +79 -0
  44. package/dist/write.d.ts.map +1 -0
  45. package/dist/write.js +84 -0
  46. package/dist/write.js.map +1 -0
  47. package/package.json +56 -0
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,239 @@
1
+ # @issuegraph/store
2
+
3
+ A framework-free client store for an [Issuegraph](https://github.com/autnmy/issuegraph)
4
+ document, and the data-source port a host plugs its own tracker in through.
5
+
6
+ **The package fetches nothing, authenticates nothing and persists nothing.** It holds the
7
+ document, renders edits optimistically, dispatches every one of them outward, and refuses to
8
+ re-evaluate the selection order until a write has actually landed.
9
+
10
+ ```
11
+ npm install @issuegraph/store
12
+ ```
13
+
14
+ Apache-2.0. `0.x`, and the API is unstable until `1.0` — a published package is a public
15
+ commitment, and this one is not making it yet.
16
+
17
+ ---
18
+
19
+ ## The five-minute version
20
+
21
+ ```ts
22
+ import { createMemorySource, createStore } from '@issuegraph/store';
23
+
24
+ const store = createStore({
25
+ source: createMemorySource({
26
+ issues: [
27
+ { ref: '1', title: 'Ship the thing', state: 'open' },
28
+ { ref: '2', title: 'Build the thing', state: 'open' },
29
+ ],
30
+ edges: [],
31
+ }),
32
+ derive: myOrderDeriver, // see "The deriver" below
33
+ });
34
+
35
+ await store.hydrate();
36
+
37
+ const handle = store.propose({ op: 'create', kind: 'blocked-by', from: '1', to: '2' });
38
+ // The edge is already drawn, marked `pending-write`.
39
+ // The order has NOT moved, and reports `status: 'held'`.
40
+
41
+ await handle.settled;
42
+ // Now the order has re-derived, and `snapshot.lastChange` says what moved.
43
+ ```
44
+
45
+ Read state with `getSnapshot()` and react to it with `subscribe(listener)` — the pair
46
+ `useSyncExternalStore` wants, so a React host needs no adapter and a non-React host is not
47
+ asked to pretend it is one.
48
+
49
+ **Every array reachable from a snapshot is frozen** — nested ones included, so a conflict
50
+ record's `upstream.edges` and an edge's `states` are as protected as `landed`. They are the
51
+ store's own state, so a `.sort()` on one would reorder that state with no dispatch and no
52
+ notification; `[...rows].sort()` is the form to use. The store copies before freezing, so
53
+ handing it an array never freezes an adapter out of its own storage. The *elements* are left
54
+ as you made them — the store owns which edges and rows it holds, you own what each one is.
55
+
56
+ ---
57
+
58
+ ## The data-source port
59
+
60
+ One interface, two methods. Everything else an adapter needs — a token, a retry policy, a
61
+ rate limiter, a cache — stays on its own side of it.
62
+
63
+ ```ts
64
+ interface DataSource {
65
+ hydrate(): Promise<GraphDocument>;
66
+ dispatch(mutation: Mutation): Promise<DispatchResult>;
67
+ }
68
+ ```
69
+
70
+ `dispatch` answers with one of four outcomes:
71
+
72
+ | outcome | what it means | what the store does |
73
+ |---|---|---|
74
+ | `applied` | the edit landed; carries the **whole** resulting document | adopts it, re-derives the order, emits a change summary |
75
+ | `unchanged` | the edit changed nothing; carries the **whole** document | adopts it, re-derives, emits **no** summary |
76
+ | `rejected` | the write was refused; carries a reason | marks the edge `failed` and offers `retry` |
77
+ | `conflict` | the document moved upstream mid-edit; carries the current one | marks the edge `conflict` and holds **both** versions |
78
+
79
+ Three contracts an adapter has to honour:
80
+
81
+ - **`applied` and `unchanged` both carry the authoritative full document — issues included —
82
+ not a patch.** A partial answer would need merge rules, and merge rules are where an
83
+ optimistic store goes wrong. Both, because both are the same kind of claim about the
84
+ document; they differ only in whether *this* edit caused the difference, which is what
85
+ decides whether a change summary is emitted. `unchanged` matters more than it looks: an
86
+ adapter often has nothing to apply precisely *because* your store is out of date — another
87
+ client got there first — and a bare "nothing to do" would leave you without an edge that
88
+ genuinely exists. An adapter that changed no issues returns the ones it holds.
89
+ - **The store dispatches once per mutation and never retries on its own.** A retry is a user
90
+ act, so nothing here assumes an idempotency you have not been given.
91
+ - **The store runs one authoritative operation at a time**, queueing the rest, so `dispatch`
92
+ is never re-entered and never overlaps a `hydrate`. Both answer with an unversioned
93
+ authoritative document, and two of those in flight cannot be ordered by anything the store
94
+ can observe — whichever answers second wins, and if that is the older one it silently rolls
95
+ an edge back out. Rather than push a version onto every adapter, the store declines to
96
+ create the situation. A queued edit still renders `pending-write` immediately; only the
97
+ round trip waits, and `rehydrate()` resolves once it has had its turn.
98
+
99
+ Because of that last one, an edit reaches the adapter some time *after* it is proposed.
100
+ `createScriptedSource` exposes `whenPending(mutationId?)` for exactly this — await the
101
+ hand-off rather than guessing how many microtasks separate the two.
102
+
103
+ Two adapters ship with the package. `createMemorySource` holds a document in a variable and
104
+ applies every edit — it is the reference implementation, and the one a demo runs on with no
105
+ tracker at all. `createScriptedSource` settles only when told to, which is what lets a test
106
+ observe the store mid-flight and induce a rejection or a conflict on purpose; write your own
107
+ adapter's tests against it.
108
+
109
+ ---
110
+
111
+ ## The deriver, and why you supply it
112
+
113
+ ```ts
114
+ type OrderDeriver = (document: GraphDocument) => readonly OrderRow[];
115
+ ```
116
+
117
+ Required, with no default. The selection order is its own concern with its own package, and a
118
+ default here would be a second implementation of it — which is exactly the duplication this
119
+ package family exists to remove.
120
+
121
+ The division of labour: **the store owns *when* the order is recomputed; the deriver owns
122
+ *what* it is.**
123
+
124
+ An optional `EdgeGuard` sits beside it for refusals that need to see the graph — a
125
+ `blocked-by` that would close a cycle:
126
+
127
+ ```ts
128
+ type EdgeGuard = (context: { mutation; current; next }) => InvalidReason | undefined;
129
+ ```
130
+
131
+ ---
132
+
133
+ ## The order can be trusted, structurally
134
+
135
+ The store keeps two edge sets:
136
+
137
+ - **`landed`** — what the data source has confirmed. **The only input to the deriver.**
138
+ - **`projected`** — `landed` plus every unsettled edit, each carrying its states. What a
139
+ viewer draws.
140
+
141
+ Optimistic rendering is allowed; optimistic **re-ordering** is not. The rail's whole value is
142
+ being what a picker will actually do, and an order reflecting an unlanded edit is a ranking
143
+ that exists nowhere. Keeping the two sets apart makes that a property of the structure rather
144
+ than a rule every reader has to remember.
145
+
146
+ While any edit is in flight, `order.status` is `'held'` and the rows are the previous ones,
147
+ unchanged — a stale-but-labelled order beats a half-computed one. Same principle if your
148
+ deriver throws: the last order that *was* derived stands, and `orderError` says why it is
149
+ stale.
150
+
151
+ ## Host callbacks that throw cannot break the store
152
+
153
+ Three of them — the deriver, the guard, and a subscriber — and one rule for all three, because
154
+ they are all your code and the store already reads a throwing *adapter* as a rejection.
155
+
156
+ A guard that throws reached **no verdict**, and an unknown verdict is not permission to write:
157
+ the edit is refused `invalid` with code `guard-failed`. A deriver that throws follows a write
158
+ that already landed, so it cannot become a failed write; the order goes stale with
159
+ `orderError` set. A subscriber that throws is isolated and rethrown asynchronously, so it
160
+ still surfaces without taking the notification loop down.
161
+
162
+ None of them may escape into the dispatch queue, where they would strand every edit behind
163
+ them with the order held for ever.
164
+
165
+ ## Edge states are overlays, not variants
166
+
167
+ An edge always keeps its kind. State is carried beside it, as a list, because the five states
168
+ are orthogonal and combine:
169
+
170
+ `selected` · `pending-write` · `invalid` · `failed` · `conflict`
171
+
172
+ `invalid` is **refused before any write** — the adapter is never called. Two things produce
173
+ it, split by what the answer needs to see:
174
+
175
+ - the store itself, for what is visible in the edit — a self-edge, a reference the document
176
+ does not hold, an exact duplicate, a retype to the kind the edge already has, a flip on a
177
+ symmetric relationship;
178
+ - your `EdgeGuard`, for anything needing the graph.
179
+
180
+ ## A failed write is marked, never reverted
181
+
182
+ The user's work stays on the canvas. `retry` re-dispatches it, and on a conflict you get the
183
+ two single-step resolutions below. **Nothing auto-merges, nothing auto-reverts, and nothing
184
+ times out** — `discardMine` is the only call that removes an optimistic edit, and a person has
185
+ to make it.
186
+
187
+ ### Resolving a conflict
188
+
189
+ A conflict's `upstream` is **for display, and is never adopted** — it is the "view diff" half
190
+ of the choice, and a reading of the past: a conflict can sit on screen for as long as a person
191
+ takes to read it.
192
+
193
+ The store ships the two single-step resolutions and **not** the composite:
194
+
195
+ ```ts
196
+ store.discardMine(id); // drop the overlay; adopts nothing
197
+ store.retry(id); // re-dispatch, unchanged
198
+ ```
199
+
200
+ **"Retry on latest" is yours to compose**, and the two lines are the point:
201
+
202
+ ```ts
203
+ await store.rehydrate();
204
+ if (store.getSnapshot().hydrationError) return; // do not retry unconfirmed
205
+ if (store.getSnapshot().writes.some((w) => w.mutationId === id)) store.retry(id);
206
+ ```
207
+
208
+ That is deliberate, and it is the one place this package asks you to write something it could
209
+ have written for you. Both calls above read state and act on it with no `await` in between, so
210
+ neither can be overtaken. The composite has an `await` in the middle, and everything that can
211
+ happen during it — the read failing, the user discarding, the user pressing again — is a
212
+ decision about *user intent* that the caller holding the await can make and the store cannot.
213
+ What a store-owned version would need — a resolution reservation, or cancellation on the
214
+ port — and why it belongs with the editor layer instead, is
215
+ [issue #7](https://github.com/autnmy/issuegraph/issues/7). The adapter is the authority on the current document; the store asks it
216
+ rather than keeping a guess about how stale its own copy has become.
217
+
218
+ `lastChange` belongs to the edit that is **current when it lands**. Two edits proposed before
219
+ the first settles means the first's summary would otherwise be written after the second began,
220
+ and a host would show the previous edit's blast radius beside the current one.
221
+
222
+ ## Two smaller contracts worth knowing
223
+
224
+ - **Issue references are opaque.** The format admits both `123` and `owner/repo#123`
225
+ (§4.2); normalising between them is the reader's job. This store compares references and
226
+ never parses one, so an adapter must emit canonical identifiers.
227
+ - **Edge identity is derived, not assigned.** It is a pure function of kind and endpoints,
228
+ with the two symmetric kinds (§4.3.4, §4.3.7) sorting theirs — so `A serialize-with B` and
229
+ `B serialize-with A` are one edge. That is what lets the store recognise the edge your
230
+ adapter returns as the one it drew.
231
+
232
+ ---
233
+
234
+ ## What this package does not do
235
+
236
+ Rendering of any kind, including the wording of the change summary — the counts ship as
237
+ numbers so a host writes the sentence in its own language. Undo, multi-select batching, the
238
+ first-pass review queue, audit findings and graph clustering all compose on top of it rather
239
+ than living in it.
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The in-memory adapter: a whole data source with no tracker behind it.
3
+ *
4
+ * It exists to make BYO-DataSource a fact rather than a claim. A seam that only
5
+ * one real adapter has ever driven is a seam by assertion, and a demo that
6
+ * needs an app installation, an auth flow and a backend proves nothing about
7
+ * whether the port is a port. This one holds a document in a variable, applies
8
+ * every edit to it, and answers immediately.
9
+ *
10
+ * It is a reference implementation, not a test double: it is the adapter a
11
+ * public demo runs on, and the shortest correct answer to "what does an adapter
12
+ * have to do?".
13
+ */
14
+ import { type GraphDocument } from '../model.ts';
15
+ import type { DataSource } from '../source.ts';
16
+ /** An in-memory data source, plus a window onto what it currently holds. */
17
+ export interface MemorySource extends DataSource {
18
+ /** The document as it stands. For a demo's "reset" button, and for tests. */
19
+ current(): GraphDocument;
20
+ }
21
+ /**
22
+ * A data source holding one document in memory.
23
+ *
24
+ * Every edit applies and nothing ever fails, which is the point: this is the
25
+ * happy path with no infrastructure at all. Inducing failure and conflict is
26
+ * {@link ../adapters/scripted.ts createScriptedSource}'s job, and keeping the
27
+ * two apart is what makes them two adapters rather than one with a switch.
28
+ */
29
+ export declare function createMemorySource(seed: GraphDocument): MemorySource;
30
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/adapters/memory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,KAAK,aAAa,EAKnB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,cAAc,CAAC;AAE/D,4EAA4E;AAC5E,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C,6EAA6E;IAC7E,OAAO,IAAI,aAAa,CAAC;CAC1B;AAwCD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,GAAG,YAAY,CAiBpE"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The in-memory adapter: a whole data source with no tracker behind it.
3
+ *
4
+ * It exists to make BYO-DataSource a fact rather than a claim. A seam that only
5
+ * one real adapter has ever driven is a seam by assertion, and a demo that
6
+ * needs an app installation, an auth flow and a backend proves nothing about
7
+ * whether the port is a port. This one holds a document in a variable, applies
8
+ * every edit to it, and answers immediately.
9
+ *
10
+ * It is a reference implementation, not a test double: it is the adapter a
11
+ * public demo runs on, and the shortest correct answer to "what does an adapter
12
+ * have to do?".
13
+ */
14
+ import { findEdge, makeEdge, sameEdgeSet, } from "../model.js";
15
+ /**
16
+ * Apply an edit to a document, or report that it changes nothing.
17
+ *
18
+ * Returns `undefined` for a no-op — a delete of an edge that is not there, a
19
+ * create of one that already is. The store has its own refusals for those, but
20
+ * an adapter cannot assume it is talking to this store, and answering
21
+ * `unchanged` is both truthful and what a real tracker would say.
22
+ */
23
+ function applied(document, mutation) {
24
+ switch (mutation.op) {
25
+ case 'create': {
26
+ const edge = makeEdge(mutation.kind, mutation.from, mutation.to);
27
+ if (findEdge(document, edge.id) !== undefined)
28
+ return undefined;
29
+ return [...document.edges, edge];
30
+ }
31
+ case 'delete': {
32
+ if (findEdge(document, mutation.edgeId) === undefined)
33
+ return undefined;
34
+ return document.edges.filter((edge) => edge.id !== mutation.edgeId);
35
+ }
36
+ case 'retype': {
37
+ const edge = findEdge(document, mutation.edgeId);
38
+ if (edge === undefined)
39
+ return undefined;
40
+ const next = makeEdge(mutation.nextKind, edge.from, edge.to);
41
+ const kept = document.edges.filter((other) => other.id !== edge.id);
42
+ if (kept.some((other) => other.id === next.id))
43
+ return kept;
44
+ return [...kept, next];
45
+ }
46
+ case 'flip': {
47
+ const edge = findEdge(document, mutation.edgeId);
48
+ if (edge === undefined)
49
+ return undefined;
50
+ const next = makeEdge(edge.kind, edge.to, edge.from);
51
+ const kept = document.edges.filter((other) => other.id !== edge.id);
52
+ if (kept.some((other) => other.id === next.id))
53
+ return kept;
54
+ return [...kept, next];
55
+ }
56
+ }
57
+ }
58
+ /**
59
+ * A data source holding one document in memory.
60
+ *
61
+ * Every edit applies and nothing ever fails, which is the point: this is the
62
+ * happy path with no infrastructure at all. Inducing failure and conflict is
63
+ * {@link ../adapters/scripted.ts createScriptedSource}'s job, and keeping the
64
+ * two apart is what makes them two adapters rather than one with a switch.
65
+ */
66
+ export function createMemorySource(seed) {
67
+ let document = { issues: [...seed.issues], edges: [...seed.edges] };
68
+ return {
69
+ current: () => document,
70
+ hydrate: () => Promise.resolve(document),
71
+ dispatch(mutation) {
72
+ const edges = applied(document, mutation);
73
+ if (edges === undefined || sameEdgeSet(document.edges, edges)) {
74
+ return Promise.resolve({ outcome: 'unchanged', document });
75
+ }
76
+ document = { issues: document.issues, edges };
77
+ return Promise.resolve({ outcome: 'applied', document });
78
+ },
79
+ };
80
+ }
81
+ //# sourceMappingURL=memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/adapters/memory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The scripted adapter: asynchronous, and able to fail on purpose.
3
+ *
4
+ * It is a second adapter rather than the in-memory one with a flag, and the
5
+ * structural difference is deliberate: **it does not settle until it is told
6
+ * to.** That is the only way to observe the store mid-flight, which is the only
7
+ * way to assert that the order stays put while a write is pending — and a flag
8
+ * on a synchronous adapter could never provide it.
9
+ *
10
+ * Two audiences. Inside this package it is what induces `failed` and `conflict`
11
+ * rather than asserting they exist. Outside it, it is the harness a host writes
12
+ * its own adapter's tests against, which is why it ships rather than living in
13
+ * a test file.
14
+ */
15
+ import type { GraphDocument } from '../model.ts';
16
+ import type { Mutation, MutationId } from '../mutation.ts';
17
+ import type { DataSource, DispatchResult } from '../source.ts';
18
+ /** An edit this adapter has been handed and has not answered yet. */
19
+ export interface PendingDispatch {
20
+ readonly mutationId: MutationId;
21
+ readonly mutation: Mutation;
22
+ }
23
+ /**
24
+ * What to answer with.
25
+ *
26
+ * `'applied'` and `'unchanged'` are resolved against the adapter's own
27
+ * document, so a caller can say "this one lands" or "there was nothing to do"
28
+ * without restating the edges. Anything else is passed through verbatim.
29
+ */
30
+ export type ScriptedOutcome = 'applied' | 'unchanged' | DispatchResult;
31
+ /** A scripted data source, plus the controls a test or a demo drives it with. */
32
+ export interface ScriptedSource extends DataSource {
33
+ /** Every edit handed over and not yet answered, oldest first. */
34
+ pending(): readonly PendingDispatch[];
35
+ /**
36
+ * Resolves once an edit is waiting to be settled — a named one, or the next
37
+ * one to arrive.
38
+ *
39
+ * A store that serialises its writes hands an edit over some time AFTER the
40
+ * caller proposed it, so "proposed" and "the adapter has it" are two
41
+ * different moments. Without this a caller has to guess how many microtasks
42
+ * separate them, which is the kind of test that passes until it doesn't.
43
+ */
44
+ whenPending(mutationId?: MutationId): Promise<PendingDispatch>;
45
+ /**
46
+ * Answer the oldest unanswered edit. Throws when there is none, because
47
+ * settling nothing is always a mistake in the caller rather than a state to
48
+ * tolerate — and a silent no-op here would make a test pass by not running.
49
+ */
50
+ settleNext(outcome: ScriptedOutcome): void;
51
+ /** Answer one specific edit, whatever order it was handed over in. */
52
+ settle(mutationId: MutationId, outcome: ScriptedOutcome): void;
53
+ /** Reject the oldest unanswered edit's promise outright — a crashing adapter. */
54
+ throwNext(error: Error): void;
55
+ /** The document as it stands. Advanced only by an `applied` settlement. */
56
+ current(): GraphDocument;
57
+ }
58
+ /**
59
+ * A data source that hands control of every settlement to its caller.
60
+ *
61
+ * `seed` is what it hydrates. `apply` computes the document an `'applied'`
62
+ * settlement produces — injected rather than reimplemented here, so a test can
63
+ * reuse an in-memory source's behaviour or supply its own without this adapter
64
+ * growing a second copy of the edit semantics.
65
+ */
66
+ export declare function createScriptedSource(seed: GraphDocument, apply: (document: GraphDocument, mutation: Mutation) => GraphDocument): ScriptedSource;
67
+ //# sourceMappingURL=scripted.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scripted.d.ts","sourceRoot":"","sources":["../../src/adapters/scripted.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE/D,qEAAqE;AACrE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,WAAW,GAAG,cAAc,CAAC;AAEvE,iFAAiF;AACjF,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,iEAAiE;IACjE,OAAO,IAAI,SAAS,eAAe,EAAE,CAAC;IACtC;;;;;;;;OAQG;IACH,WAAW,CAAC,UAAU,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC/D;;;;OAIG;IACH,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAC3C,sEAAsE;IACtE,MAAM,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAC/D,iFAAiF;IACjF,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC9B,2EAA2E;IAC3E,OAAO,IAAI,aAAa,CAAC;CAC1B;AAYD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,EACnB,KAAK,EAAE,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,KAAK,aAAa,GACpE,cAAc,CA8EhB"}