agentic-canvas 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +141 -2
- package/SKILL.md +42 -0
- package/dist/chunk-I3EJ5JB7.js +237 -0
- package/dist/chunk-NSZ2AC5O.js +932 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +150 -0
- package/dist/index.d.ts +285 -0
- package/dist/index.js +56 -0
- package/dist/store-file.d.ts +28 -0
- package/dist/store-file.js +12 -0
- package/llms.txt +13 -0
- package/package.json +77 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pooria Arab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,142 @@
|
|
|
1
|
-
#
|
|
1
|
+
# agentic-canvas
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/agentic-canvas)
|
|
4
|
+
[](https://opensource.org/license/mit)
|
|
5
|
+
|
|
6
|
+
[Package](https://www.npmjs.com/package/agentic-canvas) · [Source](https://github.com/pooriaarab/agentic-canvas) · [Support](https://github.com/pooriaarab/agentic-canvas/issues)
|
|
7
|
+
|
|
8
|
+
A shared whiteboard for diagrams, drawn by people and agents.
|
|
9
|
+
|
|
10
|
+
> Status: 0.1.0, the first release. The board model, the agent operations, the
|
|
11
|
+
> exports, the CLI, and the MCP server work. The HTTP API and the web UI are not
|
|
12
|
+
> built yet.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
Node 22 or newer.
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install agentic-canvas
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
To build from source instead, run `npm ci` and `npm run build`.
|
|
23
|
+
|
|
24
|
+
PNG export is optional. It needs the `playwright-core` package (Apache-2.0) and a
|
|
25
|
+
Chromium: `npm i playwright-core && npx playwright-core install chromium`.
|
|
26
|
+
The Chromium download is a one-time call to the Playwright CDN at install time. PNG export
|
|
27
|
+
itself makes no network call.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
export CANVAS_ACTOR=agent:helper # who writes: <kind>:<id>
|
|
33
|
+
npx agentic-canvas new --id demo --title "Release flow"
|
|
34
|
+
npx agentic-canvas flow demo --text $'direction LR\nCode(Write code) -> Test{Tests pass?}: push\nTest -> Ship[Ship it]: yes'
|
|
35
|
+
npx agentic-canvas read demo # nodes, edges, text as JSON
|
|
36
|
+
npx agentic-canvas export demo --out demo.svg
|
|
37
|
+
npx agentic-canvas export demo --format png --out demo.png
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Other commands: `add`, `connect`, `move`, `rm`. Run `npx agentic-canvas --help`.
|
|
41
|
+
`repair` is for the owner only. It needs `CANVAS_ACTOR=human:<id>`.
|
|
42
|
+
Boards live in `./.agentic-canvas` (or `--dir`, or `CANVAS_DIR`). Every write
|
|
43
|
+
needs an actor in `CANVAS_ACTOR`, as `<kind>:<id>`. The operator sets it. A write
|
|
44
|
+
fails when it is unset, empty, or has a bad kind. The CLI has no `--actor` flag and no
|
|
45
|
+
default actor, so a caller cannot pick its own kind. Only the operator can set `human`.
|
|
46
|
+
|
|
47
|
+
## MCP server
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
CANVAS_ACTOR=agent:helper npx agentic-canvas --dir ./boards mcp
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The server speaks MCP on stdio. It has one tool for each CLI command: `create_board`,
|
|
54
|
+
`read_board`, `add_shape`, `connect`, `move`, `remove`, `draw_flow`, `export_svg`, and
|
|
55
|
+
`export_png`. `repair` is not a tool. Only a human operator runs it, from the CLI. The server will not start unless `CANVAS_ACTOR` is set to a valid
|
|
56
|
+
`<kind>:<id>`. That actor stamps every write. No tool takes an actor, author, or kind, so a
|
|
57
|
+
caller cannot choose who it writes as. An error comes back as a tool error with a code.
|
|
58
|
+
|
|
59
|
+
## SDK
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { Board } from "agentic-canvas";
|
|
63
|
+
|
|
64
|
+
const me = { kind: "human", id: "ada" } as const;
|
|
65
|
+
const bot = { kind: "agent_of_human", id: "helper" } as const;
|
|
66
|
+
|
|
67
|
+
const board = new Board();
|
|
68
|
+
board.addShape({ id: "a", type: "rect", x: 0, y: 0, text: "Start" }, me);
|
|
69
|
+
board.addShape({ id: "b", type: "diamond", x: 240, y: 0, text: "Ok?" }, bot);
|
|
70
|
+
board.addConnector({ from: { id: "a" }, to: { id: "b" }, text: "next" }, bot);
|
|
71
|
+
board.list(); // every element, back to front
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Read the board as data, and draw a flowchart from text:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { drawFlow, readBoard } from "agentic-canvas";
|
|
78
|
+
|
|
79
|
+
drawFlow(board, "direction LR\nS(Start) -> D{Ok?}: go\nD -> E[Done]: yes", bot);
|
|
80
|
+
readBoard(board); // { title, nodes, edges, groups, texts }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The flow text takes one statement per line: `A -> B: label`. Use `[text]` for a
|
|
84
|
+
box, `(text)` for an ellipse, and `{text}` for a diamond. `direction LR` sets
|
|
85
|
+
the direction. Lines that start with `#` or `//` are comments.
|
|
86
|
+
|
|
87
|
+
## Features
|
|
88
|
+
|
|
89
|
+
- Shapes: rectangle, ellipse, diamond, text, sticky note, frame, image.
|
|
90
|
+
- Connectors that bind to shapes and follow them when they move.
|
|
91
|
+
- Z-order, and groups that nest.
|
|
92
|
+
- Every element keeps the kind of its author and of its last editor. An agent
|
|
93
|
+
edit stays visible as machine-made.
|
|
94
|
+
- `readBoard`: nodes, edges, groups, and text as plain data, with a machine-made
|
|
95
|
+
flag on each.
|
|
96
|
+
- `drawFlow`: a flowchart from text with a layered layout. It adds nothing when
|
|
97
|
+
anything fails.
|
|
98
|
+
- `toSvg`: a deterministic SVG. It fetches nothing and marks machine-made work
|
|
99
|
+
with `data-machine-made`.
|
|
100
|
+
- `svgToPng`: PNG through a headless Chromium with JavaScript off, service workers blocked,
|
|
101
|
+
and every network request blocked. A board over 16 million pixels exports as SVG only.
|
|
102
|
+
- A CLI over a local file store. Several processes can write at once.
|
|
103
|
+
- An MCP server (stdio) over the same service as the CLI.
|
|
104
|
+
- Yjs underneath: two copies of a board merge without conflicts.
|
|
105
|
+
- No UI code. The model is plain TypeScript on Yjs, with no outbound calls.
|
|
106
|
+
|
|
107
|
+
## Limits
|
|
108
|
+
|
|
109
|
+
- A board holds at most 10,000 elements. A flow holds at most 500 nodes and 2,000
|
|
110
|
+
edges.
|
|
111
|
+
- Flow text has no chains (`A -> B -> C`). Write one edge per line.
|
|
112
|
+
- `readBoard` leaves out embedded image bytes.
|
|
113
|
+
- Images hold a `data:` PNG, JPEG, GIF, or WebP, or a `blob:` key. Remote URLs and
|
|
114
|
+
SVG are refused.
|
|
115
|
+
- Two peers that make the same explicit id at the same time keep only one element, with no
|
|
116
|
+
error. Explicit ids are safe with one writer. Leave `id` empty and the board picks a random one.
|
|
117
|
+
- A synced update is untrusted. An element that fails the write rules is hidden, and `repair()`
|
|
118
|
+
deletes it. A board past the element cap refuses to load or read. A human operator runs
|
|
119
|
+
`repair --drop <element>` to delete elements until it fits.
|
|
120
|
+
- `update` cannot clear a required field. Only `fill` and `stroke` accept `undefined`.
|
|
121
|
+
- Z values stop at ±1e9. New elements that pass the limit land at the limit and tie with it.
|
|
122
|
+
While an element sits at +1e9, a front move fails with `LIMIT`. While one sits at -1e9, a back
|
|
123
|
+
move fails. Remove every element at the limit to clear it.
|
|
124
|
+
- Delete against connect can leave a connector with a missing end. Readers hide it. `repair()`
|
|
125
|
+
removes it, and `Board.decode` runs `repair()` for you.
|
|
126
|
+
- `move` writes absolute positions. Two peers that move one shape at the same time do not add up:
|
|
127
|
+
the last writer wins.
|
|
128
|
+
- Frames are plain shapes. They do not own the shapes inside them yet.
|
|
129
|
+
- The store is one file, `canvas.json`, in the store folder. A write takes `canvas.json.lock`.
|
|
130
|
+
A lock whose owner is gone, or that is older than 30 seconds, is taken over. A lock that
|
|
131
|
+
cannot be read counts as held: the call then waits 10 seconds and fails with `UNAVAILABLE`.
|
|
132
|
+
Remove the lock by hand if no process runs.
|
|
133
|
+
- Node 22 or newer is needed. `agentic-canvas/file-store` exports `createFileStore(path)` and
|
|
134
|
+
`openLocalStore(dir)`. A writer that is paused for more than 30 seconds can lose its lock
|
|
135
|
+
and then still write.
|
|
136
|
+
- Delete leaves no audit record yet. The caller logs it.
|
|
137
|
+
- PNG needs the optional `playwright-core` and a Chromium. Without them it fails with
|
|
138
|
+
`UNAVAILABLE`. SVG always works.
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT.
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agentic-canvas
|
|
3
|
+
description: A shared whiteboard for diagrams, drawn by people and agents. Keywords: whiteboard,canvas,diagrams,drawing,collaboration,agents,mcp,cli.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# agentic-canvas
|
|
7
|
+
|
|
8
|
+
A shared whiteboard for diagrams, drawn by people and agents.
|
|
9
|
+
|
|
10
|
+
## Use the SDK
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import { Board } from "agentic-canvas";
|
|
14
|
+
|
|
15
|
+
const bot = { kind: "agent_of_human", id: "helper" };
|
|
16
|
+
const board = new Board();
|
|
17
|
+
board.addShape({ id: "a", type: "rect", x: 0, y: 0, text: "Start" }, bot);
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Draw a flowchart from text, then read the board:
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
import { drawFlow, readBoard } from "agentic-canvas";
|
|
24
|
+
|
|
25
|
+
drawFlow(board, "A[Start] -> B{Ok?}: go\nB -> C: yes", bot);
|
|
26
|
+
readBoard(board);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Use the CLI
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
export CANVAS_ACTOR=agent:helper
|
|
33
|
+
agentic-canvas new --id demo
|
|
34
|
+
agentic-canvas flow demo --text "A -> B: go"
|
|
35
|
+
agentic-canvas read demo
|
|
36
|
+
agentic-canvas export demo --out demo.svg
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Rules
|
|
40
|
+
|
|
41
|
+
- Name the actor on every write.
|
|
42
|
+
- Report a `CanvasError` as a failure.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// src/store-file.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { mkdirSync } from "fs";
|
|
4
|
+
import { link, open, readFile, rename, unlink } from "fs/promises";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
|
|
7
|
+
// src/store-errors.ts
|
|
8
|
+
var FileStoreError = class extends Error {
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = "FileStoreError";
|
|
13
|
+
}
|
|
14
|
+
code;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/store-file.ts
|
|
18
|
+
import { assertCollection, assertId, createMemoryStore, parseRegion, StoreConflictError } from "agentic-identity";
|
|
19
|
+
var MAX_TRANSACTION_RUNS = 50;
|
|
20
|
+
var clone = (value) => JSON.parse(JSON.stringify(value));
|
|
21
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
var codeOf = (error) => error?.code;
|
|
23
|
+
function processIsAlive(pid) {
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, 0);
|
|
26
|
+
return true;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return codeOf(error) === "EPERM";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function lockIsStale(text, staleAfterMs) {
|
|
32
|
+
let held;
|
|
33
|
+
try {
|
|
34
|
+
held = JSON.parse(text);
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (typeof held !== "object" || held === null) return false;
|
|
39
|
+
const { pid, at } = held;
|
|
40
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid < 1 || typeof at !== "number" || !Number.isFinite(at)) return false;
|
|
41
|
+
return !processIsAlive(pid) || Date.now() - at > staleAfterMs;
|
|
42
|
+
}
|
|
43
|
+
function createFileStore(path, options = {}) {
|
|
44
|
+
const region = parseRegion(options.region ?? "unknown");
|
|
45
|
+
const lockPath = `${path}.lock`;
|
|
46
|
+
const timeoutMs = options.lockTimeoutMs ?? 1e4;
|
|
47
|
+
const staleAfterMs = options.staleAfterMs ?? 3e4;
|
|
48
|
+
for (const ms of [timeoutMs, staleAfterMs]) if (typeof ms !== "number" || !Number.isFinite(ms) || ms < 0) throw new TypeError("lockTimeoutMs and staleAfterMs must be finite numbers of 0 or more");
|
|
49
|
+
let queue = Promise.resolve();
|
|
50
|
+
async function load() {
|
|
51
|
+
let text;
|
|
52
|
+
try {
|
|
53
|
+
text = await readFile(path, "utf8");
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (codeOf(error) === "ENOENT") return /* @__PURE__ */ new Map();
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const data = JSON.parse(text);
|
|
60
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) throw new Error();
|
|
61
|
+
return new Map(
|
|
62
|
+
Object.entries(data).map(([name, docs]) => {
|
|
63
|
+
if (typeof docs !== "object" || docs === null || Array.isArray(docs)) throw new Error();
|
|
64
|
+
for (const doc of Object.values(docs)) if (typeof doc !== "object" || doc === null || Array.isArray(doc)) throw new Error();
|
|
65
|
+
return [name, new Map(Object.entries(docs))];
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
} catch {
|
|
69
|
+
throw new FileStoreError("DAMAGED", `the database file is not valid JSON. Repair or remove it`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function save(snapshot) {
|
|
73
|
+
const text = JSON.stringify(Object.fromEntries([...snapshot].map(([name, docs]) => [name, Object.fromEntries(docs)])));
|
|
74
|
+
const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
75
|
+
try {
|
|
76
|
+
const handle = await open(temp, "w");
|
|
77
|
+
try {
|
|
78
|
+
await handle.writeFile(text);
|
|
79
|
+
await handle.sync();
|
|
80
|
+
} finally {
|
|
81
|
+
await handle.close();
|
|
82
|
+
}
|
|
83
|
+
await rename(temp, path);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
await unlink(temp).catch(() => void 0);
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function takeOverIfStale() {
|
|
90
|
+
let seen;
|
|
91
|
+
try {
|
|
92
|
+
seen = await readFile(lockPath, "utf8");
|
|
93
|
+
} catch {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!lockIsStale(seen, staleAfterMs)) return;
|
|
97
|
+
const aside = `${lockPath}.stale.${randomUUID()}`;
|
|
98
|
+
try {
|
|
99
|
+
await rename(lockPath, aside);
|
|
100
|
+
} catch {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (await readFile(aside, "utf8").catch(() => "") !== seen) await link(aside, lockPath).catch(() => void 0);
|
|
104
|
+
await unlink(aside).catch(() => void 0);
|
|
105
|
+
}
|
|
106
|
+
async function acquire() {
|
|
107
|
+
const token = randomUUID();
|
|
108
|
+
const deadline = Date.now() + timeoutMs;
|
|
109
|
+
for (; ; ) {
|
|
110
|
+
try {
|
|
111
|
+
const handle = await open(lockPath, "wx");
|
|
112
|
+
try {
|
|
113
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, at: Date.now(), token }));
|
|
114
|
+
} finally {
|
|
115
|
+
await handle.close();
|
|
116
|
+
}
|
|
117
|
+
return token;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (codeOf(error) !== "EEXIST") throw error;
|
|
120
|
+
}
|
|
121
|
+
await takeOverIfStale();
|
|
122
|
+
if (Date.now() > deadline) throw new FileStoreError("LOCKED", `the database file is locked. If no agentic-canvas process runs, remove the .lock file next to it`);
|
|
123
|
+
await sleep(5 + Math.random() * 15);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function release(token) {
|
|
127
|
+
const text = await readFile(lockPath, "utf8").catch(() => "");
|
|
128
|
+
if (text.includes(`"token":"${token}"`)) await unlink(lockPath).catch(() => void 0);
|
|
129
|
+
}
|
|
130
|
+
async function memoryOf(snapshot, only) {
|
|
131
|
+
const memory = createMemoryStore({ region });
|
|
132
|
+
for (const [name, docs] of snapshot) if (only === void 0 || only === name) for (const [id, doc] of docs) await memory.put(name, id, doc);
|
|
133
|
+
return memory;
|
|
134
|
+
}
|
|
135
|
+
const recording = (inner, log) => ({
|
|
136
|
+
get: (collection, id) => inner.get(collection, id),
|
|
137
|
+
query: (collection, query) => inner.query(collection, query),
|
|
138
|
+
put: async (collection, id, doc) => {
|
|
139
|
+
await inner.put(collection, id, doc);
|
|
140
|
+
log.push([collection, id, clone(doc)]);
|
|
141
|
+
},
|
|
142
|
+
delete: async (collection, id) => {
|
|
143
|
+
await inner.delete(collection, id);
|
|
144
|
+
log.push([collection, id, null]);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
const locked = (body) => {
|
|
148
|
+
const run = queue.then(async () => {
|
|
149
|
+
const token = await acquire();
|
|
150
|
+
try {
|
|
151
|
+
return await body();
|
|
152
|
+
} finally {
|
|
153
|
+
await release(token);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
queue = run.catch(() => void 0);
|
|
157
|
+
return run;
|
|
158
|
+
};
|
|
159
|
+
async function commit(snapshot, log) {
|
|
160
|
+
for (const [name, id, doc] of log) {
|
|
161
|
+
if (doc === null) snapshot.get(name)?.delete(id);
|
|
162
|
+
else snapshot.set(name, (snapshot.get(name) ?? /* @__PURE__ */ new Map()).set(id, doc));
|
|
163
|
+
}
|
|
164
|
+
if (log.length > 0) await save(snapshot);
|
|
165
|
+
}
|
|
166
|
+
const writeOne = (write) => locked(async () => {
|
|
167
|
+
const log = [];
|
|
168
|
+
await write(recording(createMemoryStore({ region }), log));
|
|
169
|
+
await commit(await load(), log);
|
|
170
|
+
});
|
|
171
|
+
const watching = (inner, seen) => ({
|
|
172
|
+
...inner,
|
|
173
|
+
async get(collection, id) {
|
|
174
|
+
const doc = await inner.get(collection, id);
|
|
175
|
+
if (!seen.has(`${collection}/${id}`)) seen.set(`${collection}/${id}`, JSON.stringify(doc));
|
|
176
|
+
return doc;
|
|
177
|
+
},
|
|
178
|
+
async query(collection, query) {
|
|
179
|
+
const page = await inner.query(collection, query);
|
|
180
|
+
for (const item of page.items) if (!seen.has(`${collection}/${item.id}`)) seen.set(`${collection}/${item.id}`, JSON.stringify(item.data));
|
|
181
|
+
return page;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
const unchanged = (snapshot, seen) => [...seen].every(([key, text]) => {
|
|
185
|
+
const slash = key.indexOf("/");
|
|
186
|
+
return JSON.stringify(snapshot.get(key.slice(0, slash))?.get(key.slice(slash + 1)) ?? null) === text;
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
region,
|
|
190
|
+
async get(collection, id) {
|
|
191
|
+
assertCollection(collection);
|
|
192
|
+
assertId(id);
|
|
193
|
+
const doc = (await load()).get(collection)?.get(id);
|
|
194
|
+
return doc ? clone(doc) : null;
|
|
195
|
+
},
|
|
196
|
+
async query(collection, query) {
|
|
197
|
+
assertCollection(collection);
|
|
198
|
+
return (await memoryOf(await load(), collection)).query(collection, query);
|
|
199
|
+
},
|
|
200
|
+
put: (collection, id, doc) => writeOne((ops) => ops.put(collection, id, doc)),
|
|
201
|
+
delete: (collection, id) => writeOne((ops) => ops.delete(collection, id)),
|
|
202
|
+
// Optimistic, as the port requires: run on a copy of the file with no lock, then take the lock and commit
|
|
203
|
+
// only if every doc the run read is unchanged. Else run again. The memory adapter keeps the rule that
|
|
204
|
+
// every read comes before the first write.
|
|
205
|
+
async transaction(fn) {
|
|
206
|
+
for (let run = 0; run < MAX_TRANSACTION_RUNS; run++) {
|
|
207
|
+
const log = [];
|
|
208
|
+
const seen = /* @__PURE__ */ new Map();
|
|
209
|
+
const result = await (await memoryOf(await load())).transaction(async (tx) => {
|
|
210
|
+
log.length = 0;
|
|
211
|
+
seen.clear();
|
|
212
|
+
return fn(watching(recording(tx, log), seen));
|
|
213
|
+
});
|
|
214
|
+
const done = log.length === 0 ? unchanged(await load(), seen) : await locked(async () => {
|
|
215
|
+
const current = await load();
|
|
216
|
+
if (!unchanged(current, seen)) return false;
|
|
217
|
+
await commit(current, log);
|
|
218
|
+
return true;
|
|
219
|
+
});
|
|
220
|
+
if (done) return result;
|
|
221
|
+
}
|
|
222
|
+
throw new StoreConflictError();
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function openLocalStore(dir) {
|
|
227
|
+
mkdirSync(dir, { recursive: true });
|
|
228
|
+
return createFileStore(join(dir, STORE_FILE));
|
|
229
|
+
}
|
|
230
|
+
var STORE_FILE = "canvas.json";
|
|
231
|
+
|
|
232
|
+
export {
|
|
233
|
+
FileStoreError,
|
|
234
|
+
createFileStore,
|
|
235
|
+
openLocalStore,
|
|
236
|
+
STORE_FILE
|
|
237
|
+
};
|