@stateledger/memory 0.0.1-experimental.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 +66 -0
- package/dist/index.cjs +112 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +110 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Enow Divine
|
|
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
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @stateledger/memory
|
|
2
|
+
|
|
3
|
+
> In-memory adapter for [stateledger](https://github.com/enowdivine/stateledger).
|
|
4
|
+
> Use it for tests, hello-world demos, and prototyping — **not production**.
|
|
5
|
+
> State is lost on process exit.
|
|
6
|
+
|
|
7
|
+
> ⚠️ **Placeholder release.** This `experimental` tag exists alongside the
|
|
8
|
+
> rest of the `@stateledger/*` scope while the API stabilizes. The first
|
|
9
|
+
> real release will publish to the `latest` tag.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
pnpm add @stateledger/core @stateledger/memory
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Use
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { defineMachine } from "@stateledger/core";
|
|
23
|
+
import { InMemoryAdapter } from "@stateledger/memory";
|
|
24
|
+
|
|
25
|
+
const PaymentMachine = defineMachine({
|
|
26
|
+
name: "payment",
|
|
27
|
+
states: ["pending", "authorized", "captured", "settled"],
|
|
28
|
+
initialState: "pending",
|
|
29
|
+
transitions: [
|
|
30
|
+
{ from: "pending", to: "authorized" },
|
|
31
|
+
{ from: "authorized", to: "captured" },
|
|
32
|
+
{ from: "captured", to: "settled" },
|
|
33
|
+
],
|
|
34
|
+
} as const);
|
|
35
|
+
|
|
36
|
+
const adapter = new InMemoryAdapter();
|
|
37
|
+
const machine = PaymentMachine.for("payment-1", { adapter });
|
|
38
|
+
|
|
39
|
+
await machine.transitionTo("pending"); // bootstrap
|
|
40
|
+
await machine.transitionTo("authorized");
|
|
41
|
+
await machine.transitionTo("captured");
|
|
42
|
+
|
|
43
|
+
console.log(await machine.history());
|
|
44
|
+
// [
|
|
45
|
+
// { fromState: null, toState: "pending", sortKey: 1, ... },
|
|
46
|
+
// { fromState: "pending", toState: "authorized", sortKey: 2, ... },
|
|
47
|
+
// { fromState: "authorized", toState: "captured", sortKey: 3, ... },
|
|
48
|
+
// ]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## When to use this
|
|
52
|
+
|
|
53
|
+
- **Unit tests** in user code. Spin up a fresh adapter per test, no DB setup.
|
|
54
|
+
- **Hello-world demos** in documentation or tutorials.
|
|
55
|
+
- **Prototyping** an API design before wiring up real persistence.
|
|
56
|
+
|
|
57
|
+
## When NOT to use it
|
|
58
|
+
|
|
59
|
+
- Anything where you'd be sad if a server restart wiped the state.
|
|
60
|
+
|
|
61
|
+
For production, use [`@stateledger/prisma`](https://www.npmjs.com/package/@stateledger/prisma)
|
|
62
|
+
(coming soon) or another persistent adapter.
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@stateledger/core');
|
|
4
|
+
|
|
5
|
+
// src/in-memory-adapter.ts
|
|
6
|
+
var KeyedMutex = class {
|
|
7
|
+
queues = /* @__PURE__ */ new Map();
|
|
8
|
+
async acquire(key) {
|
|
9
|
+
let release;
|
|
10
|
+
const next = new Promise((resolve) => {
|
|
11
|
+
release = resolve;
|
|
12
|
+
});
|
|
13
|
+
const prev = this.queues.get(key) ?? Promise.resolve();
|
|
14
|
+
this.queues.set(
|
|
15
|
+
key,
|
|
16
|
+
prev.then(() => next)
|
|
17
|
+
);
|
|
18
|
+
await prev;
|
|
19
|
+
return () => {
|
|
20
|
+
if (this.queues.get(key) === prev.then(() => next)) {
|
|
21
|
+
this.queues.delete(key);
|
|
22
|
+
}
|
|
23
|
+
release();
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var InMemoryAdapter = class {
|
|
28
|
+
rows = [];
|
|
29
|
+
nextId = 1;
|
|
30
|
+
locks = new KeyedMutex();
|
|
31
|
+
// ── transaction lifecycle ──────────────────────────────────
|
|
32
|
+
async withTransaction(fn) {
|
|
33
|
+
const tx = {
|
|
34
|
+
id: `tx-${this.nextId++}`,
|
|
35
|
+
pendingAppends: [],
|
|
36
|
+
pendingPatches: /* @__PURE__ */ new Map(),
|
|
37
|
+
heldLocks: /* @__PURE__ */ new Set()
|
|
38
|
+
};
|
|
39
|
+
const releases = [];
|
|
40
|
+
tx._releases = releases;
|
|
41
|
+
try {
|
|
42
|
+
const result = await fn(tx);
|
|
43
|
+
for (const [rowId, patch] of tx.pendingPatches) {
|
|
44
|
+
const idx = this.rows.findIndex((r) => r.id === rowId);
|
|
45
|
+
if (idx >= 0) this.rows[idx] = { ...this.rows[idx], ...patch };
|
|
46
|
+
}
|
|
47
|
+
this.rows.push(...tx.pendingAppends);
|
|
48
|
+
return result;
|
|
49
|
+
} finally {
|
|
50
|
+
for (const release of releases) release();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// ── locking ────────────────────────────────────────────────
|
|
54
|
+
async acquireLock(tx, machine, subjectId) {
|
|
55
|
+
const key = `${machine}::${subjectId}`;
|
|
56
|
+
if (tx.heldLocks.has(key)) return;
|
|
57
|
+
const release = await this.locks.acquire(key);
|
|
58
|
+
tx.heldLocks.add(key);
|
|
59
|
+
tx._releases.push(release);
|
|
60
|
+
}
|
|
61
|
+
// ── reads ──────────────────────────────────────────────────
|
|
62
|
+
async readCurrent(tx, machine, subjectId) {
|
|
63
|
+
const all = this.snapshot(tx, machine, subjectId);
|
|
64
|
+
const current = all.find((r) => r.mostRecent);
|
|
65
|
+
return current ? { ...current } : null;
|
|
66
|
+
}
|
|
67
|
+
async readHistory(tx, machine, subjectId) {
|
|
68
|
+
return this.snapshot(tx, machine, subjectId).slice().sort((a, b) => a.sortKey - b.sortKey).map((r) => ({ ...r }));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Merge committed rows + this tx's pending appends/patches into a single
|
|
72
|
+
* view. Reads inside a tx see their own pending writes.
|
|
73
|
+
*/
|
|
74
|
+
snapshot(tx, machine, subjectId) {
|
|
75
|
+
const committed = this.rows.filter(
|
|
76
|
+
(r) => r.machine === machine && r.subjectId === subjectId
|
|
77
|
+
);
|
|
78
|
+
if (!tx) return committed;
|
|
79
|
+
const patched = committed.map((r) => {
|
|
80
|
+
const patch = tx.pendingPatches.get(r.id);
|
|
81
|
+
return patch ? { ...r, ...patch } : r;
|
|
82
|
+
});
|
|
83
|
+
const pending = tx.pendingAppends.filter(
|
|
84
|
+
(r) => r.machine === machine && r.subjectId === subjectId
|
|
85
|
+
);
|
|
86
|
+
return [...patched, ...pending];
|
|
87
|
+
}
|
|
88
|
+
// ── writes ─────────────────────────────────────────────────
|
|
89
|
+
async appendTransition(tx, row) {
|
|
90
|
+
try {
|
|
91
|
+
const previousCurrent = await this.readCurrent(tx, row.machine, row.subjectId) ?? null;
|
|
92
|
+
if (previousCurrent) {
|
|
93
|
+
tx.pendingPatches.set(previousCurrent.id, { mostRecent: false });
|
|
94
|
+
}
|
|
95
|
+
const inserted = {
|
|
96
|
+
...row,
|
|
97
|
+
id: `txn-${this.nextId++}`,
|
|
98
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
99
|
+
};
|
|
100
|
+
tx.pendingAppends.push(inserted);
|
|
101
|
+
return { ...inserted };
|
|
102
|
+
} catch (err) {
|
|
103
|
+
throw new core.AdapterError("in-memory append failed", { cause: err });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async updateSubjectState(_tx, _hint, _newState) {
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
exports.InMemoryAdapter = InMemoryAdapter;
|
|
111
|
+
//# sourceMappingURL=index.cjs.map
|
|
112
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/in-memory-adapter.ts"],"names":["AdapterError"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * In-memory adapter for stateledger.\n *\n * Backed by a plain `Map`. Used by the core test suite to validate the\n * contract test pack itself and as a teaching fixture for adapter authors.\n *\n * Implements `Adapter<InMemoryTx>` with pessimistic semantics:\n * - acquireLock uses a per-`(machine, subjectId)` promise queue so two\n * concurrent transactions cannot hold the lock at once.\n * - appendTransition flips the prior mostRecent inside the same\n * transactional buffer.\n * - withTransaction commits the buffer on success, discards on throw.\n *\n * Not thread-safe in any meaningful sense; this is a teaching adapter, not\n * a production store.\n */\n\nimport type {\n Adapter,\n NewTransitionRow,\n SubjectStateHint,\n TransitionRow,\n} from \"@stateledger/core\";\nimport { AdapterError } from \"@stateledger/core\";\n\ntype StoredRow = TransitionRow;\n\n/** Opaque transaction handle. The adapter knows how to use it; users don't. */\nexport type InMemoryTx = {\n readonly id: string;\n /** Pending inserts staged inside this tx — applied to the store on commit. */\n pendingAppends: StoredRow[];\n /** Per-row patches (e.g. flipping mostRecent) staged inside this tx. */\n pendingPatches: Map<string, Partial<StoredRow>>;\n /** Locks held by this tx, released on commit/rollback. */\n heldLocks: Set<string>;\n};\n\n/** A simple FIFO mutex per key, used to serialize lock acquisition. */\nclass KeyedMutex {\n private queues = new Map<string, Promise<void>>();\n\n async acquire(key: string): Promise<() => void> {\n let release!: () => void;\n const next = new Promise<void>((resolve) => {\n release = resolve;\n });\n const prev = this.queues.get(key) ?? Promise.resolve();\n this.queues.set(\n key,\n prev.then(() => next),\n );\n await prev;\n return () => {\n // If we're the tail, clean the entry so the map doesn't grow forever.\n if (this.queues.get(key) === prev.then(() => next)) {\n this.queues.delete(key);\n }\n release();\n };\n }\n}\n\nexport class InMemoryAdapter implements Adapter<InMemoryTx> {\n private rows: StoredRow[] = [];\n private nextId = 1;\n private locks = new KeyedMutex();\n\n // ── transaction lifecycle ──────────────────────────────────\n\n async withTransaction<R>(fn: (tx: InMemoryTx) => Promise<R>): Promise<R> {\n const tx: InMemoryTx = {\n id: `tx-${this.nextId++}`,\n pendingAppends: [],\n pendingPatches: new Map(),\n heldLocks: new Set(),\n };\n\n const releases: Array<() => void> = [];\n // The InMemoryTx tracks its own held locks via heldLocks; we also keep\n // a parallel array of release callbacks so we can free them on\n // commit/rollback without re-querying the mutex.\n (tx as InMemoryTx & { _releases: Array<() => void> })._releases = releases;\n\n // On success: apply pending writes atomically.\n // On throw: pending buffer is dropped — that's the rollback.\n try {\n const result = await fn(tx);\n for (const [rowId, patch] of tx.pendingPatches) {\n const idx = this.rows.findIndex((r) => r.id === rowId);\n if (idx >= 0) this.rows[idx] = { ...this.rows[idx]!, ...patch } as StoredRow;\n }\n this.rows.push(...tx.pendingAppends);\n return result;\n } finally {\n for (const release of releases) release();\n }\n }\n\n // ── locking ────────────────────────────────────────────────\n\n async acquireLock(tx: InMemoryTx, machine: string, subjectId: string): Promise<void> {\n const key = `${machine}::${subjectId}`;\n if (tx.heldLocks.has(key)) return; // re-entrant within same tx\n const release = await this.locks.acquire(key);\n tx.heldLocks.add(key);\n (tx as InMemoryTx & { _releases: Array<() => void> })._releases.push(release);\n }\n\n // ── reads ──────────────────────────────────────────────────\n\n async readCurrent(\n tx: InMemoryTx | null,\n machine: string,\n subjectId: string,\n ): Promise<TransitionRow | null> {\n const all = this.snapshot(tx, machine, subjectId);\n const current = all.find((r) => r.mostRecent);\n return current ? { ...current } : null;\n }\n\n async readHistory(\n tx: InMemoryTx | null,\n machine: string,\n subjectId: string,\n ): Promise<TransitionRow[]> {\n return this.snapshot(tx, machine, subjectId)\n .slice()\n .sort((a, b) => a.sortKey - b.sortKey)\n .map((r) => ({ ...r }));\n }\n\n /**\n * Merge committed rows + this tx's pending appends/patches into a single\n * view. Reads inside a tx see their own pending writes.\n */\n private snapshot(\n tx: InMemoryTx | null,\n machine: string,\n subjectId: string,\n ): StoredRow[] {\n const committed = this.rows.filter(\n (r) => r.machine === machine && r.subjectId === subjectId,\n );\n if (!tx) return committed;\n\n const patched = committed.map((r) => {\n const patch = tx.pendingPatches.get(r.id);\n return patch ? ({ ...r, ...patch } as StoredRow) : r;\n });\n const pending = tx.pendingAppends.filter(\n (r) => r.machine === machine && r.subjectId === subjectId,\n );\n return [...patched, ...pending];\n }\n\n // ── writes ─────────────────────────────────────────────────\n\n async appendTransition(tx: InMemoryTx, row: NewTransitionRow): Promise<TransitionRow> {\n try {\n // Flip the previous mostRecent (in the tx's pending buffer, not committed yet).\n const previousCurrent = (await this.readCurrent(tx, row.machine, row.subjectId)) ?? null;\n if (previousCurrent) {\n tx.pendingPatches.set(previousCurrent.id, { mostRecent: false });\n }\n\n const inserted: StoredRow = {\n ...row,\n id: `txn-${this.nextId++}`,\n createdAt: new Date(),\n };\n tx.pendingAppends.push(inserted);\n return { ...inserted };\n } catch (err) {\n throw new AdapterError(\"in-memory append failed\", { cause: err });\n }\n }\n\n async updateSubjectState(\n _tx: InMemoryTx,\n _hint: SubjectStateHint,\n _newState: string,\n ): Promise<void> {\n // Optional method — the in-memory adapter has no subject row to update.\n }\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Adapter, TransitionRow, NewTransitionRow, SubjectStateHint } from '@stateledger/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory adapter for stateledger.
|
|
5
|
+
*
|
|
6
|
+
* Backed by a plain `Map`. Used by the core test suite to validate the
|
|
7
|
+
* contract test pack itself and as a teaching fixture for adapter authors.
|
|
8
|
+
*
|
|
9
|
+
* Implements `Adapter<InMemoryTx>` with pessimistic semantics:
|
|
10
|
+
* - acquireLock uses a per-`(machine, subjectId)` promise queue so two
|
|
11
|
+
* concurrent transactions cannot hold the lock at once.
|
|
12
|
+
* - appendTransition flips the prior mostRecent inside the same
|
|
13
|
+
* transactional buffer.
|
|
14
|
+
* - withTransaction commits the buffer on success, discards on throw.
|
|
15
|
+
*
|
|
16
|
+
* Not thread-safe in any meaningful sense; this is a teaching adapter, not
|
|
17
|
+
* a production store.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
type StoredRow = TransitionRow;
|
|
21
|
+
/** Opaque transaction handle. The adapter knows how to use it; users don't. */
|
|
22
|
+
type InMemoryTx = {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
/** Pending inserts staged inside this tx — applied to the store on commit. */
|
|
25
|
+
pendingAppends: StoredRow[];
|
|
26
|
+
/** Per-row patches (e.g. flipping mostRecent) staged inside this tx. */
|
|
27
|
+
pendingPatches: Map<string, Partial<StoredRow>>;
|
|
28
|
+
/** Locks held by this tx, released on commit/rollback. */
|
|
29
|
+
heldLocks: Set<string>;
|
|
30
|
+
};
|
|
31
|
+
declare class InMemoryAdapter implements Adapter<InMemoryTx> {
|
|
32
|
+
private rows;
|
|
33
|
+
private nextId;
|
|
34
|
+
private locks;
|
|
35
|
+
withTransaction<R>(fn: (tx: InMemoryTx) => Promise<R>): Promise<R>;
|
|
36
|
+
acquireLock(tx: InMemoryTx, machine: string, subjectId: string): Promise<void>;
|
|
37
|
+
readCurrent(tx: InMemoryTx | null, machine: string, subjectId: string): Promise<TransitionRow | null>;
|
|
38
|
+
readHistory(tx: InMemoryTx | null, machine: string, subjectId: string): Promise<TransitionRow[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Merge committed rows + this tx's pending appends/patches into a single
|
|
41
|
+
* view. Reads inside a tx see their own pending writes.
|
|
42
|
+
*/
|
|
43
|
+
private snapshot;
|
|
44
|
+
appendTransition(tx: InMemoryTx, row: NewTransitionRow): Promise<TransitionRow>;
|
|
45
|
+
updateSubjectState(_tx: InMemoryTx, _hint: SubjectStateHint, _newState: string): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { InMemoryAdapter, type InMemoryTx };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Adapter, TransitionRow, NewTransitionRow, SubjectStateHint } from '@stateledger/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory adapter for stateledger.
|
|
5
|
+
*
|
|
6
|
+
* Backed by a plain `Map`. Used by the core test suite to validate the
|
|
7
|
+
* contract test pack itself and as a teaching fixture for adapter authors.
|
|
8
|
+
*
|
|
9
|
+
* Implements `Adapter<InMemoryTx>` with pessimistic semantics:
|
|
10
|
+
* - acquireLock uses a per-`(machine, subjectId)` promise queue so two
|
|
11
|
+
* concurrent transactions cannot hold the lock at once.
|
|
12
|
+
* - appendTransition flips the prior mostRecent inside the same
|
|
13
|
+
* transactional buffer.
|
|
14
|
+
* - withTransaction commits the buffer on success, discards on throw.
|
|
15
|
+
*
|
|
16
|
+
* Not thread-safe in any meaningful sense; this is a teaching adapter, not
|
|
17
|
+
* a production store.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
type StoredRow = TransitionRow;
|
|
21
|
+
/** Opaque transaction handle. The adapter knows how to use it; users don't. */
|
|
22
|
+
type InMemoryTx = {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
/** Pending inserts staged inside this tx — applied to the store on commit. */
|
|
25
|
+
pendingAppends: StoredRow[];
|
|
26
|
+
/** Per-row patches (e.g. flipping mostRecent) staged inside this tx. */
|
|
27
|
+
pendingPatches: Map<string, Partial<StoredRow>>;
|
|
28
|
+
/** Locks held by this tx, released on commit/rollback. */
|
|
29
|
+
heldLocks: Set<string>;
|
|
30
|
+
};
|
|
31
|
+
declare class InMemoryAdapter implements Adapter<InMemoryTx> {
|
|
32
|
+
private rows;
|
|
33
|
+
private nextId;
|
|
34
|
+
private locks;
|
|
35
|
+
withTransaction<R>(fn: (tx: InMemoryTx) => Promise<R>): Promise<R>;
|
|
36
|
+
acquireLock(tx: InMemoryTx, machine: string, subjectId: string): Promise<void>;
|
|
37
|
+
readCurrent(tx: InMemoryTx | null, machine: string, subjectId: string): Promise<TransitionRow | null>;
|
|
38
|
+
readHistory(tx: InMemoryTx | null, machine: string, subjectId: string): Promise<TransitionRow[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Merge committed rows + this tx's pending appends/patches into a single
|
|
41
|
+
* view. Reads inside a tx see their own pending writes.
|
|
42
|
+
*/
|
|
43
|
+
private snapshot;
|
|
44
|
+
appendTransition(tx: InMemoryTx, row: NewTransitionRow): Promise<TransitionRow>;
|
|
45
|
+
updateSubjectState(_tx: InMemoryTx, _hint: SubjectStateHint, _newState: string): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { InMemoryAdapter, type InMemoryTx };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { AdapterError } from '@stateledger/core';
|
|
2
|
+
|
|
3
|
+
// src/in-memory-adapter.ts
|
|
4
|
+
var KeyedMutex = class {
|
|
5
|
+
queues = /* @__PURE__ */ new Map();
|
|
6
|
+
async acquire(key) {
|
|
7
|
+
let release;
|
|
8
|
+
const next = new Promise((resolve) => {
|
|
9
|
+
release = resolve;
|
|
10
|
+
});
|
|
11
|
+
const prev = this.queues.get(key) ?? Promise.resolve();
|
|
12
|
+
this.queues.set(
|
|
13
|
+
key,
|
|
14
|
+
prev.then(() => next)
|
|
15
|
+
);
|
|
16
|
+
await prev;
|
|
17
|
+
return () => {
|
|
18
|
+
if (this.queues.get(key) === prev.then(() => next)) {
|
|
19
|
+
this.queues.delete(key);
|
|
20
|
+
}
|
|
21
|
+
release();
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var InMemoryAdapter = class {
|
|
26
|
+
rows = [];
|
|
27
|
+
nextId = 1;
|
|
28
|
+
locks = new KeyedMutex();
|
|
29
|
+
// ── transaction lifecycle ──────────────────────────────────
|
|
30
|
+
async withTransaction(fn) {
|
|
31
|
+
const tx = {
|
|
32
|
+
id: `tx-${this.nextId++}`,
|
|
33
|
+
pendingAppends: [],
|
|
34
|
+
pendingPatches: /* @__PURE__ */ new Map(),
|
|
35
|
+
heldLocks: /* @__PURE__ */ new Set()
|
|
36
|
+
};
|
|
37
|
+
const releases = [];
|
|
38
|
+
tx._releases = releases;
|
|
39
|
+
try {
|
|
40
|
+
const result = await fn(tx);
|
|
41
|
+
for (const [rowId, patch] of tx.pendingPatches) {
|
|
42
|
+
const idx = this.rows.findIndex((r) => r.id === rowId);
|
|
43
|
+
if (idx >= 0) this.rows[idx] = { ...this.rows[idx], ...patch };
|
|
44
|
+
}
|
|
45
|
+
this.rows.push(...tx.pendingAppends);
|
|
46
|
+
return result;
|
|
47
|
+
} finally {
|
|
48
|
+
for (const release of releases) release();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// ── locking ────────────────────────────────────────────────
|
|
52
|
+
async acquireLock(tx, machine, subjectId) {
|
|
53
|
+
const key = `${machine}::${subjectId}`;
|
|
54
|
+
if (tx.heldLocks.has(key)) return;
|
|
55
|
+
const release = await this.locks.acquire(key);
|
|
56
|
+
tx.heldLocks.add(key);
|
|
57
|
+
tx._releases.push(release);
|
|
58
|
+
}
|
|
59
|
+
// ── reads ──────────────────────────────────────────────────
|
|
60
|
+
async readCurrent(tx, machine, subjectId) {
|
|
61
|
+
const all = this.snapshot(tx, machine, subjectId);
|
|
62
|
+
const current = all.find((r) => r.mostRecent);
|
|
63
|
+
return current ? { ...current } : null;
|
|
64
|
+
}
|
|
65
|
+
async readHistory(tx, machine, subjectId) {
|
|
66
|
+
return this.snapshot(tx, machine, subjectId).slice().sort((a, b) => a.sortKey - b.sortKey).map((r) => ({ ...r }));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Merge committed rows + this tx's pending appends/patches into a single
|
|
70
|
+
* view. Reads inside a tx see their own pending writes.
|
|
71
|
+
*/
|
|
72
|
+
snapshot(tx, machine, subjectId) {
|
|
73
|
+
const committed = this.rows.filter(
|
|
74
|
+
(r) => r.machine === machine && r.subjectId === subjectId
|
|
75
|
+
);
|
|
76
|
+
if (!tx) return committed;
|
|
77
|
+
const patched = committed.map((r) => {
|
|
78
|
+
const patch = tx.pendingPatches.get(r.id);
|
|
79
|
+
return patch ? { ...r, ...patch } : r;
|
|
80
|
+
});
|
|
81
|
+
const pending = tx.pendingAppends.filter(
|
|
82
|
+
(r) => r.machine === machine && r.subjectId === subjectId
|
|
83
|
+
);
|
|
84
|
+
return [...patched, ...pending];
|
|
85
|
+
}
|
|
86
|
+
// ── writes ─────────────────────────────────────────────────
|
|
87
|
+
async appendTransition(tx, row) {
|
|
88
|
+
try {
|
|
89
|
+
const previousCurrent = await this.readCurrent(tx, row.machine, row.subjectId) ?? null;
|
|
90
|
+
if (previousCurrent) {
|
|
91
|
+
tx.pendingPatches.set(previousCurrent.id, { mostRecent: false });
|
|
92
|
+
}
|
|
93
|
+
const inserted = {
|
|
94
|
+
...row,
|
|
95
|
+
id: `txn-${this.nextId++}`,
|
|
96
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
97
|
+
};
|
|
98
|
+
tx.pendingAppends.push(inserted);
|
|
99
|
+
return { ...inserted };
|
|
100
|
+
} catch (err) {
|
|
101
|
+
throw new AdapterError("in-memory append failed", { cause: err });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async updateSubjectState(_tx, _hint, _newState) {
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export { InMemoryAdapter };
|
|
109
|
+
//# sourceMappingURL=index.js.map
|
|
110
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/in-memory-adapter.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * In-memory adapter for stateledger.\n *\n * Backed by a plain `Map`. Used by the core test suite to validate the\n * contract test pack itself and as a teaching fixture for adapter authors.\n *\n * Implements `Adapter<InMemoryTx>` with pessimistic semantics:\n * - acquireLock uses a per-`(machine, subjectId)` promise queue so two\n * concurrent transactions cannot hold the lock at once.\n * - appendTransition flips the prior mostRecent inside the same\n * transactional buffer.\n * - withTransaction commits the buffer on success, discards on throw.\n *\n * Not thread-safe in any meaningful sense; this is a teaching adapter, not\n * a production store.\n */\n\nimport type {\n Adapter,\n NewTransitionRow,\n SubjectStateHint,\n TransitionRow,\n} from \"@stateledger/core\";\nimport { AdapterError } from \"@stateledger/core\";\n\ntype StoredRow = TransitionRow;\n\n/** Opaque transaction handle. The adapter knows how to use it; users don't. */\nexport type InMemoryTx = {\n readonly id: string;\n /** Pending inserts staged inside this tx — applied to the store on commit. */\n pendingAppends: StoredRow[];\n /** Per-row patches (e.g. flipping mostRecent) staged inside this tx. */\n pendingPatches: Map<string, Partial<StoredRow>>;\n /** Locks held by this tx, released on commit/rollback. */\n heldLocks: Set<string>;\n};\n\n/** A simple FIFO mutex per key, used to serialize lock acquisition. */\nclass KeyedMutex {\n private queues = new Map<string, Promise<void>>();\n\n async acquire(key: string): Promise<() => void> {\n let release!: () => void;\n const next = new Promise<void>((resolve) => {\n release = resolve;\n });\n const prev = this.queues.get(key) ?? Promise.resolve();\n this.queues.set(\n key,\n prev.then(() => next),\n );\n await prev;\n return () => {\n // If we're the tail, clean the entry so the map doesn't grow forever.\n if (this.queues.get(key) === prev.then(() => next)) {\n this.queues.delete(key);\n }\n release();\n };\n }\n}\n\nexport class InMemoryAdapter implements Adapter<InMemoryTx> {\n private rows: StoredRow[] = [];\n private nextId = 1;\n private locks = new KeyedMutex();\n\n // ── transaction lifecycle ──────────────────────────────────\n\n async withTransaction<R>(fn: (tx: InMemoryTx) => Promise<R>): Promise<R> {\n const tx: InMemoryTx = {\n id: `tx-${this.nextId++}`,\n pendingAppends: [],\n pendingPatches: new Map(),\n heldLocks: new Set(),\n };\n\n const releases: Array<() => void> = [];\n // The InMemoryTx tracks its own held locks via heldLocks; we also keep\n // a parallel array of release callbacks so we can free them on\n // commit/rollback without re-querying the mutex.\n (tx as InMemoryTx & { _releases: Array<() => void> })._releases = releases;\n\n // On success: apply pending writes atomically.\n // On throw: pending buffer is dropped — that's the rollback.\n try {\n const result = await fn(tx);\n for (const [rowId, patch] of tx.pendingPatches) {\n const idx = this.rows.findIndex((r) => r.id === rowId);\n if (idx >= 0) this.rows[idx] = { ...this.rows[idx]!, ...patch } as StoredRow;\n }\n this.rows.push(...tx.pendingAppends);\n return result;\n } finally {\n for (const release of releases) release();\n }\n }\n\n // ── locking ────────────────────────────────────────────────\n\n async acquireLock(tx: InMemoryTx, machine: string, subjectId: string): Promise<void> {\n const key = `${machine}::${subjectId}`;\n if (tx.heldLocks.has(key)) return; // re-entrant within same tx\n const release = await this.locks.acquire(key);\n tx.heldLocks.add(key);\n (tx as InMemoryTx & { _releases: Array<() => void> })._releases.push(release);\n }\n\n // ── reads ──────────────────────────────────────────────────\n\n async readCurrent(\n tx: InMemoryTx | null,\n machine: string,\n subjectId: string,\n ): Promise<TransitionRow | null> {\n const all = this.snapshot(tx, machine, subjectId);\n const current = all.find((r) => r.mostRecent);\n return current ? { ...current } : null;\n }\n\n async readHistory(\n tx: InMemoryTx | null,\n machine: string,\n subjectId: string,\n ): Promise<TransitionRow[]> {\n return this.snapshot(tx, machine, subjectId)\n .slice()\n .sort((a, b) => a.sortKey - b.sortKey)\n .map((r) => ({ ...r }));\n }\n\n /**\n * Merge committed rows + this tx's pending appends/patches into a single\n * view. Reads inside a tx see their own pending writes.\n */\n private snapshot(\n tx: InMemoryTx | null,\n machine: string,\n subjectId: string,\n ): StoredRow[] {\n const committed = this.rows.filter(\n (r) => r.machine === machine && r.subjectId === subjectId,\n );\n if (!tx) return committed;\n\n const patched = committed.map((r) => {\n const patch = tx.pendingPatches.get(r.id);\n return patch ? ({ ...r, ...patch } as StoredRow) : r;\n });\n const pending = tx.pendingAppends.filter(\n (r) => r.machine === machine && r.subjectId === subjectId,\n );\n return [...patched, ...pending];\n }\n\n // ── writes ─────────────────────────────────────────────────\n\n async appendTransition(tx: InMemoryTx, row: NewTransitionRow): Promise<TransitionRow> {\n try {\n // Flip the previous mostRecent (in the tx's pending buffer, not committed yet).\n const previousCurrent = (await this.readCurrent(tx, row.machine, row.subjectId)) ?? null;\n if (previousCurrent) {\n tx.pendingPatches.set(previousCurrent.id, { mostRecent: false });\n }\n\n const inserted: StoredRow = {\n ...row,\n id: `txn-${this.nextId++}`,\n createdAt: new Date(),\n };\n tx.pendingAppends.push(inserted);\n return { ...inserted };\n } catch (err) {\n throw new AdapterError(\"in-memory append failed\", { cause: err });\n }\n }\n\n async updateSubjectState(\n _tx: InMemoryTx,\n _hint: SubjectStateHint,\n _newState: string,\n ): Promise<void> {\n // Optional method — the in-memory adapter has no subject row to update.\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stateledger/memory",
|
|
3
|
+
"version": "0.0.1-experimental.0",
|
|
4
|
+
"description": "In-memory adapter for stateledger — for tests, hello-world demos, and prototyping.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Enow Divine",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/enowdivine/stateledger.git",
|
|
10
|
+
"directory": "packages/memory"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/enowdivine/stateledger#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/enowdivine/stateledger/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"state-machine",
|
|
18
|
+
"in-memory",
|
|
19
|
+
"stateledger",
|
|
20
|
+
"testing",
|
|
21
|
+
"fixture"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./dist/index.cjs",
|
|
25
|
+
"module": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"require": "./dist/index.cjs"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=20"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@stateledger/core": "0.0.1-experimental.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsup",
|
|
49
|
+
"dev": "tsup --watch",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"clean": "rm -rf dist"
|
|
52
|
+
}
|
|
53
|
+
}
|