@optimystic/db-p2p 0.0.1 → 0.1.2
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/{readme.md → README.md} +7 -0
- package/dist/index.min.js +31 -30
- package/dist/index.min.js.map +4 -4
- package/dist/src/cluster/cluster-repo.d.ts +27 -0
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +129 -17
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/service.d.ts +13 -2
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +17 -7
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-node.d.ts +13 -2
- package/dist/src/libp2p-node.d.ts.map +1 -1
- package/dist/src/libp2p-node.js +40 -17
- package/dist/src/libp2p-node.js.map +1 -1
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +8 -7
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +7 -2
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +18 -3
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +26 -3
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +117 -22
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/service.d.ts +13 -2
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +25 -12
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/storage/memory-storage.d.ts +15 -0
- package/dist/src/storage/memory-storage.d.ts.map +1 -1
- package/dist/src/storage/memory-storage.js +23 -4
- package/dist/src/storage/memory-storage.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/sync/service.d.ts.map +1 -1
- package/dist/src/sync/service.js +7 -2
- package/dist/src/sync/service.js.map +1 -1
- package/package.json +27 -20
- package/src/cluster/cluster-repo.ts +828 -711
- package/src/cluster/service.ts +44 -31
- package/src/index.ts +1 -1
- package/src/libp2p-key-network.ts +334 -334
- package/src/libp2p-node.ts +371 -335
- package/src/network/network-manager-service.ts +334 -334
- package/src/protocol-client.ts +53 -54
- package/src/repo/client.ts +112 -112
- package/src/repo/cluster-coordinator.ts +613 -592
- package/src/repo/coordinator-repo.ts +269 -137
- package/src/repo/service.ts +237 -219
- package/src/storage/block-storage.ts +182 -182
- package/src/storage/memory-storage.ts +24 -5
- package/src/storage/storage-repo.ts +321 -320
- package/src/sync/service.ts +7 -6
- package/dist/src/storage/file-storage.d.ts +0 -30
- package/dist/src/storage/file-storage.d.ts.map +0 -1
- package/dist/src/storage/file-storage.js +0 -127
- package/dist/src/storage/file-storage.js.map +0 -1
- package/src/storage/file-storage.ts +0 -163
|
@@ -1,182 +1,182 @@
|
|
|
1
|
-
import type { BlockId, IBlock, Transform, ActionId, ActionRev } from "@optimystic/db-core";
|
|
2
|
-
import { Latches, applyTransform } from "@optimystic/db-core";
|
|
3
|
-
import type { BlockArchive, BlockMetadata, RestoreCallback, RevisionRange } from "./struct.js";
|
|
4
|
-
import type { IRawStorage } from "./i-raw-storage.js";
|
|
5
|
-
import { mergeRanges } from "./helpers.js";
|
|
6
|
-
import type { IBlockStorage } from "./i-block-storage.js";
|
|
7
|
-
|
|
8
|
-
export class BlockStorage implements IBlockStorage {
|
|
9
|
-
constructor(
|
|
10
|
-
private readonly blockId: BlockId,
|
|
11
|
-
private readonly storage: IRawStorage,
|
|
12
|
-
private readonly restoreCallback?: RestoreCallback
|
|
13
|
-
) { }
|
|
14
|
-
|
|
15
|
-
async getLatest(): Promise<ActionRev | undefined> {
|
|
16
|
-
const meta = await this.storage.getMetadata(this.blockId);
|
|
17
|
-
return meta?.latest;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async getBlock(rev?: number): Promise<{ block: IBlock, actionRev: ActionRev } | undefined> {
|
|
21
|
-
const meta = await this.storage.getMetadata(this.blockId);
|
|
22
|
-
if (!meta) {
|
|
23
|
-
return undefined;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const targetRev = rev ?? meta.latest?.rev;
|
|
27
|
-
if (targetRev === undefined) {
|
|
28
|
-
throw new Error(`No revision specified and no latest revision exists for block ${this.blockId}`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
await this.ensureRevision(meta, targetRev);
|
|
32
|
-
return await this.materializeBlock(meta, targetRev);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async getTransaction(actionId: ActionId): Promise<Transform | undefined> {
|
|
36
|
-
return await this.storage.getTransaction(this.blockId, actionId);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async getPendingTransaction(actionId: ActionId): Promise<Transform | undefined> {
|
|
40
|
-
return await this.storage.getPendingTransaction(this.blockId, actionId);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async *listPendingTransactions(): AsyncIterable<ActionId> {
|
|
44
|
-
yield* this.storage.listPendingTransactions(this.blockId);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async savePendingTransaction(actionId: ActionId, transform: Transform): Promise<void> {
|
|
48
|
-
let meta = await this.storage.getMetadata(this.blockId);
|
|
49
|
-
if (!meta) {
|
|
50
|
-
meta = { latest: undefined, ranges: [[0]] };
|
|
51
|
-
await this.storage.saveMetadata(this.blockId, meta);
|
|
52
|
-
}
|
|
53
|
-
await this.storage.savePendingTransaction(this.blockId, actionId, transform);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async deletePendingTransaction(actionId: ActionId): Promise<void> {
|
|
57
|
-
await this.storage.deletePendingTransaction(this.blockId, actionId);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async *listRevisions(startRev: number, endRev: number): AsyncIterable<ActionRev> {
|
|
61
|
-
yield* this.storage.listRevisions(this.blockId, startRev, endRev);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async saveMaterializedBlock(actionId: ActionId, block: IBlock | undefined): Promise<void> {
|
|
65
|
-
await this.storage.saveMaterializedBlock(this.blockId, actionId, block);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async saveRevision(rev: number, actionId: ActionId): Promise<void> {
|
|
69
|
-
await this.storage.saveRevision(this.blockId, rev, actionId);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async promotePendingTransaction(actionId: ActionId): Promise<void> {
|
|
73
|
-
await this.storage.promotePendingTransaction(this.blockId, actionId);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async setLatest(latest: ActionRev): Promise<void> {
|
|
77
|
-
const meta = await this.storage.getMetadata(this.blockId);
|
|
78
|
-
if (!meta) {
|
|
79
|
-
throw new Error(`Block ${this.blockId} not found`);
|
|
80
|
-
}
|
|
81
|
-
meta.latest = latest;
|
|
82
|
-
await this.storage.saveMetadata(this.blockId, meta);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
private async ensureRevision(meta: BlockMetadata, rev: number): Promise<void> {
|
|
86
|
-
if (this.inRanges(rev, meta.ranges)) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const lockId = `BlockStorage.ensureRevision:${this.blockId}`;
|
|
91
|
-
const release = await Latches.acquire(lockId);
|
|
92
|
-
try {
|
|
93
|
-
const currentMeta = await this.storage.getMetadata(this.blockId);
|
|
94
|
-
if (!currentMeta) {
|
|
95
|
-
throw new Error(`Block ${this.blockId} metadata disappeared unexpectedly.`);
|
|
96
|
-
}
|
|
97
|
-
if (this.inRanges(rev, currentMeta.ranges)) {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const restored = await this.restoreBlock(rev);
|
|
102
|
-
if (!restored) {
|
|
103
|
-
throw new Error(`Block ${this.blockId} revision ${rev} not found during restore attempt.`);
|
|
104
|
-
}
|
|
105
|
-
await this.saveRestored(restored);
|
|
106
|
-
|
|
107
|
-
currentMeta.ranges.unshift(restored.range);
|
|
108
|
-
currentMeta.ranges = mergeRanges(currentMeta.ranges);
|
|
109
|
-
await this.storage.saveMetadata(this.blockId, currentMeta);
|
|
110
|
-
|
|
111
|
-
} finally {
|
|
112
|
-
release();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
private async materializeBlock(_meta: BlockMetadata, targetRev: number): Promise<{ block: IBlock, actionRev: ActionRev }> {
|
|
117
|
-
let block: IBlock | undefined;
|
|
118
|
-
let materializedActionRev: ActionRev | undefined;
|
|
119
|
-
const actions: ActionRev[] = [];
|
|
120
|
-
|
|
121
|
-
// Find the materialized block
|
|
122
|
-
for await (const actionRev of this.storage.listRevisions(this.blockId, targetRev, 1)) {
|
|
123
|
-
const materializedBlock = await this.storage.getMaterializedBlock(this.blockId, actionRev.actionId);
|
|
124
|
-
if (materializedBlock) {
|
|
125
|
-
block = materializedBlock;
|
|
126
|
-
materializedActionRev = actionRev;
|
|
127
|
-
break;
|
|
128
|
-
} else {
|
|
129
|
-
actions.push(actionRev);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (!block || !materializedActionRev) {
|
|
134
|
-
// There is an implicit requirement that there must be a materialization of the block somewhere in it's history. If the log is truncated, a materialization must be made at the truncation point..
|
|
135
|
-
throw new Error(`Failed to find materialized block ${this.blockId} for revision ${targetRev}`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Apply transforms in reverse order
|
|
139
|
-
for (let i = actions.length - 1; i >= 0; --i) {
|
|
140
|
-
const { actionId } = actions[i]!;
|
|
141
|
-
const transform = await this.storage.getTransaction(this.blockId, actionId);
|
|
142
|
-
if (!transform) {
|
|
143
|
-
throw new Error(`Missing action ${actionId} for block ${this.blockId}`);
|
|
144
|
-
}
|
|
145
|
-
block = applyTransform(block, transform);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (!block) {
|
|
149
|
-
throw new Error(`Block ${this.blockId} has been deleted`);
|
|
150
|
-
}
|
|
151
|
-
if (actions.length) {
|
|
152
|
-
await this.storage.saveMaterializedBlock(this.blockId, actions[0]!.actionId, block);
|
|
153
|
-
return { block, actionRev: actions[0]! };
|
|
154
|
-
}
|
|
155
|
-
return { block, actionRev: materializedActionRev };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
private async restoreBlock(rev: number): Promise<BlockArchive | undefined> {
|
|
159
|
-
if (!this.restoreCallback) return undefined;
|
|
160
|
-
return await this.restoreCallback(this.blockId, rev);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
private async saveRestored(archive: BlockArchive) {
|
|
164
|
-
const revisions = Object.entries(archive.revisions)
|
|
165
|
-
.map(([rev, data]) => ({ rev: Number(rev), data }));
|
|
166
|
-
|
|
167
|
-
// Save all revisions, actions, and materializations
|
|
168
|
-
for (const { rev, data: { action, block } } of revisions) {
|
|
169
|
-
await Promise.all([
|
|
170
|
-
this.storage.saveRevision(this.blockId, rev, action.actionId),
|
|
171
|
-
this.storage.saveTransaction(this.blockId, action.actionId, action.transform),
|
|
172
|
-
block ? this.storage.saveMaterializedBlock(this.blockId, action.actionId, block) : Promise.resolve()
|
|
173
|
-
]);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
private inRanges(rev: number, ranges: RevisionRange[]): boolean {
|
|
178
|
-
return ranges.some(range =>
|
|
179
|
-
rev >= range[0] && (range[1] === undefined || rev < range[1])
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
1
|
+
import type { BlockId, IBlock, Transform, ActionId, ActionRev } from "@optimystic/db-core";
|
|
2
|
+
import { Latches, applyTransform } from "@optimystic/db-core";
|
|
3
|
+
import type { BlockArchive, BlockMetadata, RestoreCallback, RevisionRange } from "./struct.js";
|
|
4
|
+
import type { IRawStorage } from "./i-raw-storage.js";
|
|
5
|
+
import { mergeRanges } from "./helpers.js";
|
|
6
|
+
import type { IBlockStorage } from "./i-block-storage.js";
|
|
7
|
+
|
|
8
|
+
export class BlockStorage implements IBlockStorage {
|
|
9
|
+
constructor(
|
|
10
|
+
private readonly blockId: BlockId,
|
|
11
|
+
private readonly storage: IRawStorage,
|
|
12
|
+
private readonly restoreCallback?: RestoreCallback
|
|
13
|
+
) { }
|
|
14
|
+
|
|
15
|
+
async getLatest(): Promise<ActionRev | undefined> {
|
|
16
|
+
const meta = await this.storage.getMetadata(this.blockId);
|
|
17
|
+
return meta?.latest;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async getBlock(rev?: number): Promise<{ block: IBlock, actionRev: ActionRev } | undefined> {
|
|
21
|
+
const meta = await this.storage.getMetadata(this.blockId);
|
|
22
|
+
if (!meta) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const targetRev = rev ?? meta.latest?.rev;
|
|
27
|
+
if (targetRev === undefined) {
|
|
28
|
+
throw new Error(`No revision specified and no latest revision exists for block ${this.blockId}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await this.ensureRevision(meta, targetRev);
|
|
32
|
+
return await this.materializeBlock(meta, targetRev);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async getTransaction(actionId: ActionId): Promise<Transform | undefined> {
|
|
36
|
+
return await this.storage.getTransaction(this.blockId, actionId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async getPendingTransaction(actionId: ActionId): Promise<Transform | undefined> {
|
|
40
|
+
return await this.storage.getPendingTransaction(this.blockId, actionId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async *listPendingTransactions(): AsyncIterable<ActionId> {
|
|
44
|
+
yield* this.storage.listPendingTransactions(this.blockId);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async savePendingTransaction(actionId: ActionId, transform: Transform): Promise<void> {
|
|
48
|
+
let meta = await this.storage.getMetadata(this.blockId);
|
|
49
|
+
if (!meta) {
|
|
50
|
+
meta = { latest: undefined, ranges: [[0]] };
|
|
51
|
+
await this.storage.saveMetadata(this.blockId, meta);
|
|
52
|
+
}
|
|
53
|
+
await this.storage.savePendingTransaction(this.blockId, actionId, transform);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async deletePendingTransaction(actionId: ActionId): Promise<void> {
|
|
57
|
+
await this.storage.deletePendingTransaction(this.blockId, actionId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async *listRevisions(startRev: number, endRev: number): AsyncIterable<ActionRev> {
|
|
61
|
+
yield* this.storage.listRevisions(this.blockId, startRev, endRev);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async saveMaterializedBlock(actionId: ActionId, block: IBlock | undefined): Promise<void> {
|
|
65
|
+
await this.storage.saveMaterializedBlock(this.blockId, actionId, block);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async saveRevision(rev: number, actionId: ActionId): Promise<void> {
|
|
69
|
+
await this.storage.saveRevision(this.blockId, rev, actionId);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async promotePendingTransaction(actionId: ActionId): Promise<void> {
|
|
73
|
+
await this.storage.promotePendingTransaction(this.blockId, actionId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async setLatest(latest: ActionRev): Promise<void> {
|
|
77
|
+
const meta = await this.storage.getMetadata(this.blockId);
|
|
78
|
+
if (!meta) {
|
|
79
|
+
throw new Error(`Block ${this.blockId} not found`);
|
|
80
|
+
}
|
|
81
|
+
meta.latest = latest;
|
|
82
|
+
await this.storage.saveMetadata(this.blockId, meta);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async ensureRevision(meta: BlockMetadata, rev: number): Promise<void> {
|
|
86
|
+
if (this.inRanges(rev, meta.ranges)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const lockId = `BlockStorage.ensureRevision:${this.blockId}`;
|
|
91
|
+
const release = await Latches.acquire(lockId);
|
|
92
|
+
try {
|
|
93
|
+
const currentMeta = await this.storage.getMetadata(this.blockId);
|
|
94
|
+
if (!currentMeta) {
|
|
95
|
+
throw new Error(`Block ${this.blockId} metadata disappeared unexpectedly.`);
|
|
96
|
+
}
|
|
97
|
+
if (this.inRanges(rev, currentMeta.ranges)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const restored = await this.restoreBlock(rev);
|
|
102
|
+
if (!restored) {
|
|
103
|
+
throw new Error(`Block ${this.blockId} revision ${rev} not found during restore attempt.`);
|
|
104
|
+
}
|
|
105
|
+
await this.saveRestored(restored);
|
|
106
|
+
|
|
107
|
+
currentMeta.ranges.unshift(restored.range);
|
|
108
|
+
currentMeta.ranges = mergeRanges(currentMeta.ranges);
|
|
109
|
+
await this.storage.saveMetadata(this.blockId, currentMeta);
|
|
110
|
+
|
|
111
|
+
} finally {
|
|
112
|
+
release();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async materializeBlock(_meta: BlockMetadata, targetRev: number): Promise<{ block: IBlock, actionRev: ActionRev }> {
|
|
117
|
+
let block: IBlock | undefined;
|
|
118
|
+
let materializedActionRev: ActionRev | undefined;
|
|
119
|
+
const actions: ActionRev[] = [];
|
|
120
|
+
|
|
121
|
+
// Find the materialized block
|
|
122
|
+
for await (const actionRev of this.storage.listRevisions(this.blockId, targetRev, 1)) {
|
|
123
|
+
const materializedBlock = await this.storage.getMaterializedBlock(this.blockId, actionRev.actionId);
|
|
124
|
+
if (materializedBlock) {
|
|
125
|
+
block = materializedBlock;
|
|
126
|
+
materializedActionRev = actionRev;
|
|
127
|
+
break;
|
|
128
|
+
} else {
|
|
129
|
+
actions.push(actionRev);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!block || !materializedActionRev) {
|
|
134
|
+
// There is an implicit requirement that there must be a materialization of the block somewhere in it's history. If the log is truncated, a materialization must be made at the truncation point..
|
|
135
|
+
throw new Error(`Failed to find materialized block ${this.blockId} for revision ${targetRev}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Apply transforms in reverse order
|
|
139
|
+
for (let i = actions.length - 1; i >= 0; --i) {
|
|
140
|
+
const { actionId } = actions[i]!;
|
|
141
|
+
const transform = await this.storage.getTransaction(this.blockId, actionId);
|
|
142
|
+
if (!transform) {
|
|
143
|
+
throw new Error(`Missing action ${actionId} for block ${this.blockId}`);
|
|
144
|
+
}
|
|
145
|
+
block = applyTransform(block, transform);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!block) {
|
|
149
|
+
throw new Error(`Block ${this.blockId} has been deleted`);
|
|
150
|
+
}
|
|
151
|
+
if (actions.length) {
|
|
152
|
+
await this.storage.saveMaterializedBlock(this.blockId, actions[0]!.actionId, block);
|
|
153
|
+
return { block, actionRev: actions[0]! };
|
|
154
|
+
}
|
|
155
|
+
return { block, actionRev: materializedActionRev };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async restoreBlock(rev: number): Promise<BlockArchive | undefined> {
|
|
159
|
+
if (!this.restoreCallback) return undefined;
|
|
160
|
+
return await this.restoreCallback(this.blockId, rev);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async saveRestored(archive: BlockArchive) {
|
|
164
|
+
const revisions = Object.entries(archive.revisions)
|
|
165
|
+
.map(([rev, data]) => ({ rev: Number(rev), data }));
|
|
166
|
+
|
|
167
|
+
// Save all revisions, actions, and materializations
|
|
168
|
+
for (const { rev, data: { action, block } } of revisions) {
|
|
169
|
+
await Promise.all([
|
|
170
|
+
this.storage.saveRevision(this.blockId, rev, action.actionId),
|
|
171
|
+
this.storage.saveTransaction(this.blockId, action.actionId, action.transform),
|
|
172
|
+
block ? this.storage.saveMaterializedBlock(this.blockId, action.actionId, block) : Promise.resolve()
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private inRanges(rev: number, ranges: RevisionRange[]): boolean {
|
|
178
|
+
return ranges.some(range =>
|
|
179
|
+
rev >= range[0] && (range[1] === undefined || rev < range[1])
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -60,7 +60,8 @@ export class MemoryRawStorage implements IRawStorage {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
async savePendingTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
|
|
63
|
-
|
|
63
|
+
// Clone transform to prevent external modifications from affecting stored data
|
|
64
|
+
this.pendingActions.set(this.getActionKey(blockId, actionId), structuredClone(transform));
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
async deletePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
@@ -84,14 +85,32 @@ export class MemoryRawStorage implements IRawStorage {
|
|
|
84
85
|
this.actions.set(this.getActionKey(blockId, actionId), transform);
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Retrieves a materialized block at a specific revision.
|
|
90
|
+
*
|
|
91
|
+
* @pitfall **MUST return a clone** - `applyTransform()` mutates blocks in place.
|
|
92
|
+
* If we return the stored reference, mutations corrupt ALL revisions that share
|
|
93
|
+
* the same underlying object.
|
|
94
|
+
* @see docs/internals.md "Storage Returns References" pitfall
|
|
95
|
+
*/
|
|
87
96
|
async getMaterializedBlock(blockId: BlockId, actionId: ActionId): Promise<IBlock | undefined> {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
const block = this.materializedBlocks.get(this.getActionKey(blockId, actionId));
|
|
98
|
+
// Clone to prevent external mutations from affecting stored data
|
|
99
|
+
return block ? structuredClone(block) : undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Stores a materialized block at a specific revision.
|
|
104
|
+
*
|
|
105
|
+
* @pitfall **MUST store a clone** - callers may continue mutating the block after saving.
|
|
106
|
+
* If we store the reference, those mutations corrupt the stored data.
|
|
107
|
+
* @see docs/internals.md "Storage Returns References" pitfall
|
|
108
|
+
*/
|
|
91
109
|
async saveMaterializedBlock(blockId: BlockId, actionId: ActionId, block?: IBlock): Promise<void> {
|
|
92
110
|
const key = this.getActionKey(blockId, actionId);
|
|
93
111
|
if (block) {
|
|
94
|
-
|
|
112
|
+
// Clone to prevent external mutations from affecting stored data
|
|
113
|
+
this.materializedBlocks.set(key, structuredClone(block));
|
|
95
114
|
} else {
|
|
96
115
|
this.materializedBlocks.delete(key);
|
|
97
116
|
}
|