@optimystic/db-p2p 0.0.1
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/dist/index.min.js +52 -0
- package/dist/index.min.js.map +7 -0
- package/dist/src/cluster/client.d.ts +12 -0
- package/dist/src/cluster/client.d.ts.map +1 -0
- package/dist/src/cluster/client.js +65 -0
- package/dist/src/cluster/client.js.map +1 -0
- package/dist/src/cluster/cluster-repo.d.ts +79 -0
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -0
- package/dist/src/cluster/cluster-repo.js +613 -0
- package/dist/src/cluster/cluster-repo.js.map +1 -0
- package/dist/src/cluster/partition-detector.d.ts +59 -0
- package/dist/src/cluster/partition-detector.d.ts.map +1 -0
- package/dist/src/cluster/partition-detector.js +129 -0
- package/dist/src/cluster/partition-detector.js.map +1 -0
- package/dist/src/cluster/service.d.ts +49 -0
- package/dist/src/cluster/service.d.ts.map +1 -0
- package/dist/src/cluster/service.js +107 -0
- package/dist/src/cluster/service.js.map +1 -0
- package/dist/src/index.d.ts +29 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +29 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/it-utility.d.ts +4 -0
- package/dist/src/it-utility.d.ts.map +1 -0
- package/dist/src/it-utility.js +32 -0
- package/dist/src/it-utility.js.map +1 -0
- package/dist/src/libp2p-key-network.d.ts +59 -0
- package/dist/src/libp2p-key-network.d.ts.map +1 -0
- package/dist/src/libp2p-key-network.js +278 -0
- package/dist/src/libp2p-key-network.js.map +1 -0
- package/dist/src/libp2p-node.d.ts +28 -0
- package/dist/src/libp2p-node.d.ts.map +1 -0
- package/dist/src/libp2p-node.js +270 -0
- package/dist/src/libp2p-node.js.map +1 -0
- package/dist/src/logger.d.ts +3 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +6 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/network/get-network-manager.d.ts +4 -0
- package/dist/src/network/get-network-manager.d.ts.map +1 -0
- package/dist/src/network/get-network-manager.js +17 -0
- package/dist/src/network/get-network-manager.js.map +1 -0
- package/dist/src/network/network-manager-service.d.ts +82 -0
- package/dist/src/network/network-manager-service.d.ts.map +1 -0
- package/dist/src/network/network-manager-service.js +283 -0
- package/dist/src/network/network-manager-service.js.map +1 -0
- package/dist/src/peer-utils.d.ts +2 -0
- package/dist/src/peer-utils.d.ts.map +1 -0
- package/dist/src/peer-utils.js +28 -0
- package/dist/src/peer-utils.js.map +1 -0
- package/dist/src/protocol-client.d.ts +12 -0
- package/dist/src/protocol-client.d.ts.map +1 -0
- package/dist/src/protocol-client.js +34 -0
- package/dist/src/protocol-client.js.map +1 -0
- package/dist/src/repo/client.d.ts +17 -0
- package/dist/src/repo/client.d.ts.map +1 -0
- package/dist/src/repo/client.js +82 -0
- package/dist/src/repo/client.js.map +1 -0
- package/dist/src/repo/cluster-coordinator.d.ts +59 -0
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -0
- package/dist/src/repo/cluster-coordinator.js +539 -0
- package/dist/src/repo/cluster-coordinator.js.map +1 -0
- package/dist/src/repo/coordinator-repo.d.ts +29 -0
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -0
- package/dist/src/repo/coordinator-repo.js +102 -0
- package/dist/src/repo/coordinator-repo.js.map +1 -0
- package/dist/src/repo/redirect.d.ts +14 -0
- package/dist/src/repo/redirect.d.ts.map +1 -0
- package/dist/src/repo/redirect.js +9 -0
- package/dist/src/repo/redirect.js.map +1 -0
- package/dist/src/repo/service.d.ts +52 -0
- package/dist/src/repo/service.d.ts.map +1 -0
- package/dist/src/repo/service.js +181 -0
- package/dist/src/repo/service.js.map +1 -0
- package/dist/src/repo/types.d.ts +7 -0
- package/dist/src/repo/types.d.ts.map +1 -0
- package/dist/src/repo/types.js +2 -0
- package/dist/src/repo/types.js.map +1 -0
- package/dist/src/routing/libp2p-known-peers.d.ts +4 -0
- package/dist/src/routing/libp2p-known-peers.d.ts.map +1 -0
- package/dist/src/routing/libp2p-known-peers.js +19 -0
- package/dist/src/routing/libp2p-known-peers.js.map +1 -0
- package/dist/src/routing/responsibility.d.ts +14 -0
- package/dist/src/routing/responsibility.d.ts.map +1 -0
- package/dist/src/routing/responsibility.js +45 -0
- package/dist/src/routing/responsibility.js.map +1 -0
- package/dist/src/routing/simple-cluster-coordinator.d.ts +23 -0
- package/dist/src/routing/simple-cluster-coordinator.d.ts.map +1 -0
- package/dist/src/routing/simple-cluster-coordinator.js +59 -0
- package/dist/src/routing/simple-cluster-coordinator.js.map +1 -0
- package/dist/src/storage/arachnode-fret-adapter.d.ts +65 -0
- package/dist/src/storage/arachnode-fret-adapter.d.ts.map +1 -0
- package/dist/src/storage/arachnode-fret-adapter.js +93 -0
- package/dist/src/storage/arachnode-fret-adapter.js.map +1 -0
- package/dist/src/storage/block-storage.d.ts +31 -0
- package/dist/src/storage/block-storage.d.ts.map +1 -0
- package/dist/src/storage/block-storage.js +154 -0
- package/dist/src/storage/block-storage.js.map +1 -0
- package/dist/src/storage/file-storage.d.ts +30 -0
- package/dist/src/storage/file-storage.d.ts.map +1 -0
- package/dist/src/storage/file-storage.js +127 -0
- package/dist/src/storage/file-storage.js.map +1 -0
- package/dist/src/storage/helpers.d.ts +3 -0
- package/dist/src/storage/helpers.d.ts.map +1 -0
- package/dist/src/storage/helpers.js +28 -0
- package/dist/src/storage/helpers.js.map +1 -0
- package/dist/src/storage/i-block-storage.d.ts +32 -0
- package/dist/src/storage/i-block-storage.d.ts.map +1 -0
- package/dist/src/storage/i-block-storage.js +2 -0
- package/dist/src/storage/i-block-storage.js.map +1 -0
- package/dist/src/storage/i-raw-storage.d.ts +20 -0
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -0
- package/dist/src/storage/i-raw-storage.js +2 -0
- package/dist/src/storage/i-raw-storage.js.map +1 -0
- package/dist/src/storage/memory-storage.d.ts +27 -0
- package/dist/src/storage/memory-storage.d.ts.map +1 -0
- package/dist/src/storage/memory-storage.js +87 -0
- package/dist/src/storage/memory-storage.js.map +1 -0
- package/dist/src/storage/restoration-coordinator-v2.d.ts +63 -0
- package/dist/src/storage/restoration-coordinator-v2.d.ts.map +1 -0
- package/dist/src/storage/restoration-coordinator-v2.js +157 -0
- package/dist/src/storage/restoration-coordinator-v2.js.map +1 -0
- package/dist/src/storage/ring-selector.d.ts +56 -0
- package/dist/src/storage/ring-selector.d.ts.map +1 -0
- package/dist/src/storage/ring-selector.js +118 -0
- package/dist/src/storage/ring-selector.js.map +1 -0
- package/dist/src/storage/storage-monitor.d.ts +23 -0
- package/dist/src/storage/storage-monitor.d.ts.map +1 -0
- package/dist/src/storage/storage-monitor.js +40 -0
- package/dist/src/storage/storage-monitor.js.map +1 -0
- package/dist/src/storage/storage-repo.d.ts +17 -0
- package/dist/src/storage/storage-repo.d.ts.map +1 -0
- package/dist/src/storage/storage-repo.js +267 -0
- package/dist/src/storage/storage-repo.js.map +1 -0
- package/dist/src/storage/struct.d.ts +29 -0
- package/dist/src/storage/struct.d.ts.map +1 -0
- package/dist/src/storage/struct.js +2 -0
- package/dist/src/storage/struct.js.map +1 -0
- package/dist/src/sync/client.d.ts +27 -0
- package/dist/src/sync/client.d.ts.map +1 -0
- package/dist/src/sync/client.js +32 -0
- package/dist/src/sync/client.js.map +1 -0
- package/dist/src/sync/protocol.d.ts +58 -0
- package/dist/src/sync/protocol.d.ts.map +1 -0
- package/dist/src/sync/protocol.js +12 -0
- package/dist/src/sync/protocol.js.map +1 -0
- package/dist/src/sync/service.d.ts +62 -0
- package/dist/src/sync/service.d.ts.map +1 -0
- package/dist/src/sync/service.js +168 -0
- package/dist/src/sync/service.js.map +1 -0
- package/package.json +73 -0
- package/readme.md +497 -0
- package/src/cluster/client.ts +63 -0
- package/src/cluster/cluster-repo.ts +711 -0
- package/src/cluster/partition-detector.ts +158 -0
- package/src/cluster/service.ts +156 -0
- package/src/index.ts +30 -0
- package/src/it-utility.ts +36 -0
- package/src/libp2p-key-network.ts +334 -0
- package/src/libp2p-node.ts +335 -0
- package/src/logger.ts +9 -0
- package/src/network/get-network-manager.ts +17 -0
- package/src/network/network-manager-service.ts +334 -0
- package/src/peer-utils.ts +24 -0
- package/src/protocol-client.ts +54 -0
- package/src/repo/client.ts +112 -0
- package/src/repo/cluster-coordinator.ts +592 -0
- package/src/repo/coordinator-repo.ts +137 -0
- package/src/repo/redirect.ts +17 -0
- package/src/repo/service.ts +219 -0
- package/src/repo/types.ts +7 -0
- package/src/routing/libp2p-known-peers.ts +26 -0
- package/src/routing/responsibility.ts +63 -0
- package/src/routing/simple-cluster-coordinator.ts +70 -0
- package/src/storage/arachnode-fret-adapter.ts +128 -0
- package/src/storage/block-storage.ts +182 -0
- package/src/storage/file-storage.ts +163 -0
- package/src/storage/helpers.ts +29 -0
- package/src/storage/i-block-storage.ts +40 -0
- package/src/storage/i-raw-storage.ts +30 -0
- package/src/storage/memory-storage.ts +108 -0
- package/src/storage/restoration-coordinator-v2.ts +191 -0
- package/src/storage/ring-selector.ts +155 -0
- package/src/storage/storage-monitor.ts +59 -0
- package/src/storage/storage-repo.ts +320 -0
- package/src/storage/struct.ts +34 -0
- package/src/sync/client.ts +42 -0
- package/src/sync/protocol.ts +71 -0
- package/src/sync/service.ts +229 -0
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import type { BlockId, IBlock, Transform, ActionId, ActionRev } from "@optimystic/db-core";
|
|
4
|
+
import type { BlockMetadata } from "./struct.js";
|
|
5
|
+
import type { IRawStorage } from "./i-raw-storage.js";
|
|
6
|
+
import { createLogger } from '../logger.js'
|
|
7
|
+
|
|
8
|
+
const log = createLogger('storage:file')
|
|
9
|
+
|
|
10
|
+
export class FileRawStorage implements IRawStorage {
|
|
11
|
+
constructor(private readonly basePath: string) {
|
|
12
|
+
// TODO: use https://www.npmjs.com/package/proper-lockfile to take a lock on the basePath, also introduce explicit dispose pattern
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async getMetadata(blockId: BlockId): Promise<BlockMetadata | undefined> {
|
|
16
|
+
return this.readIfExists<BlockMetadata>(this.getMetadataPath(blockId));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async saveMetadata(blockId: BlockId, metadata: BlockMetadata): Promise<void> {
|
|
20
|
+
await this.ensureAndWriteFile(
|
|
21
|
+
this.getMetadataPath(blockId),
|
|
22
|
+
JSON.stringify(metadata)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getRevision(blockId: BlockId, rev: number): Promise<ActionId | undefined> {
|
|
27
|
+
return this.readIfExists<ActionId>(this.getRevisionPath(blockId, rev));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async saveRevision(blockId: BlockId, rev: number, actionId: ActionId): Promise<void> {
|
|
31
|
+
await this.ensureAndWriteFile(
|
|
32
|
+
this.getRevisionPath(blockId, rev),
|
|
33
|
+
actionId
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async getPendingTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
|
|
38
|
+
return this.readIfExists<Transform>(this.getPendingActionPath(blockId, actionId));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async savePendingTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
|
|
42
|
+
await this.ensureAndWriteFile(
|
|
43
|
+
this.getPendingActionPath(blockId, actionId),
|
|
44
|
+
JSON.stringify(transform)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async deletePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
49
|
+
const pendingPath = this.getPendingActionPath(blockId, actionId);
|
|
50
|
+
await fs.unlink(pendingPath)
|
|
51
|
+
.catch((err) => {
|
|
52
|
+
// Ignore if file doesn't exist
|
|
53
|
+
if ((err as NodeJS.ErrnoException)?.code !== 'ENOENT') log('deletePendingTransaction unlink failed for %s/%s - %o', blockId, actionId, err)
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async *listPendingTransactions(blockId: BlockId): AsyncIterable<ActionId> {
|
|
58
|
+
const pendingPath = path.join(this.getBlockPath(blockId), 'pend');
|
|
59
|
+
|
|
60
|
+
const files = await fs.readdir(pendingPath).catch((err) => { log('listPendingTransactions readdir failed for %s - %o', blockId, err); return [] as string[] });
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
if (!file.endsWith('.json')) continue;
|
|
63
|
+
const rawActionId = file.slice(0, -5);
|
|
64
|
+
if (!/^[\w\d]+-[\w\d]+-[\w\d]+-[\w\d]+-[\w\d]+$/.test(rawActionId)) continue;
|
|
65
|
+
yield rawActionId as ActionId;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
|
|
70
|
+
return this.readIfExists<Transform>(this.getActionPath(blockId, actionId));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async *listRevisions(blockId: BlockId, startRev: number, endRev: number): AsyncIterable<ActionRev> {
|
|
74
|
+
// TODO: Optimize this for sparse revs
|
|
75
|
+
for (let rev = startRev; startRev <= endRev ? rev <= endRev : rev >= endRev; startRev <= endRev ? ++rev : --rev) {
|
|
76
|
+
const actionId = await this.getRevision(blockId, rev);
|
|
77
|
+
if (actionId) {
|
|
78
|
+
yield { actionId, rev };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async saveTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
|
|
84
|
+
await this.ensureAndWriteFile(
|
|
85
|
+
this.getActionPath(blockId, actionId),
|
|
86
|
+
JSON.stringify(transform)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async getMaterializedBlock(blockId: BlockId, actionId: ActionId): Promise<IBlock | undefined> {
|
|
91
|
+
return this.readIfExists<IBlock>(this.getMaterializedPath(blockId, actionId));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async saveMaterializedBlock(blockId: BlockId, actionId: ActionId, block?: IBlock): Promise<void> {
|
|
95
|
+
if (block) {
|
|
96
|
+
await this.ensureAndWriteFile(
|
|
97
|
+
this.getMaterializedPath(blockId, actionId),
|
|
98
|
+
JSON.stringify(block)
|
|
99
|
+
);
|
|
100
|
+
} else {
|
|
101
|
+
await fs.unlink(this.getMaterializedPath(blockId, actionId))
|
|
102
|
+
.catch((err) => {
|
|
103
|
+
// Ignore if file doesn't exist
|
|
104
|
+
if ((err as NodeJS.ErrnoException)?.code !== 'ENOENT') log('saveMaterializedBlock unlink failed for %s/%s - %o', blockId, actionId, err)
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async promotePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
110
|
+
const pendingPath = this.getPendingActionPath(blockId, actionId);
|
|
111
|
+
const actionPath = this.getActionPath(blockId, actionId);
|
|
112
|
+
|
|
113
|
+
// Ensure target directory exists
|
|
114
|
+
await fs.mkdir(path.dirname(actionPath), { recursive: true });
|
|
115
|
+
|
|
116
|
+
return fs.rename(pendingPath, actionPath)
|
|
117
|
+
.catch(err => {
|
|
118
|
+
if (err.code === 'ENOENT') {
|
|
119
|
+
throw new Error(`Pending action ${actionId} not found for block ${blockId}`);
|
|
120
|
+
}
|
|
121
|
+
log('promotePendingTransaction rename failed for %s/%s - %o', blockId, actionId, err)
|
|
122
|
+
throw err;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private getBlockPath(blockId: BlockId): string {
|
|
127
|
+
return path.join(this.basePath, blockId);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private getMetadataPath(blockId: BlockId): string {
|
|
131
|
+
return path.join(this.getBlockPath(blockId), 'meta.json');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private getRevisionPath(blockId: BlockId, rev: number): string {
|
|
135
|
+
return path.join(this.getBlockPath(blockId), 'revs', `${rev}.json`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private getPendingActionPath(blockId: BlockId, actionId: ActionId): string {
|
|
139
|
+
return path.join(this.getBlockPath(blockId), 'pend', `${actionId}.json`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private getActionPath(blockId: BlockId, actionId: ActionId): string {
|
|
143
|
+
return path.join(this.getBlockPath(blockId), 'actions', `${actionId}.json`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private getMaterializedPath(blockId: BlockId, actionId: ActionId): string {
|
|
147
|
+
return path.join(this.getBlockPath(blockId), 'blocks', `${actionId}.json`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private async readIfExists<T>(filePath: string): Promise<T | undefined> {
|
|
151
|
+
return fs.readFile(filePath, 'utf-8')
|
|
152
|
+
.then(content => JSON.parse(content) as T)
|
|
153
|
+
.catch(err => {
|
|
154
|
+
if (err.code === 'ENOENT') return undefined;
|
|
155
|
+
throw err;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async ensureAndWriteFile(filePath: string, content: string): Promise<void> {
|
|
160
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
161
|
+
await fs.writeFile(filePath, content);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RevisionRange } from "./struct.js";
|
|
2
|
+
|
|
3
|
+
export function mergeRanges(ranges: RevisionRange[]): RevisionRange[] {
|
|
4
|
+
if (ranges.length <= 1) return ranges;
|
|
5
|
+
|
|
6
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
7
|
+
const merged: RevisionRange[] = [ranges[0]!];
|
|
8
|
+
|
|
9
|
+
for (const range of ranges.slice(1)) {
|
|
10
|
+
const last = merged[merged.length - 1]!;
|
|
11
|
+
// If last range is open-ended, it consumes all following ranges
|
|
12
|
+
if (last[1] === undefined) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
// If this range starts at or before last range's end (exclusive)
|
|
16
|
+
if (range[0] <= last[1]) {
|
|
17
|
+
// If this range is open-ended, make last range open-ended
|
|
18
|
+
if (range[1] === undefined) {
|
|
19
|
+
last[1] = undefined;
|
|
20
|
+
} else {
|
|
21
|
+
last[1] = Math.max(last[1], range[1]);
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
merged.push(range);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return merged;
|
|
29
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { IBlock, Transform, ActionId, ActionRev } from "@optimystic/db-core";
|
|
2
|
+
|
|
3
|
+
/** Interface for block-level storage operations */
|
|
4
|
+
export interface IBlockStorage {
|
|
5
|
+
/** Gets the latest revision information for this block */
|
|
6
|
+
getLatest(): Promise<ActionRev | undefined>;
|
|
7
|
+
|
|
8
|
+
/** Gets a materialized block at the given revision */
|
|
9
|
+
getBlock(rev?: number): Promise<{ block: IBlock, actionRev: ActionRev } | undefined>;
|
|
10
|
+
|
|
11
|
+
/** Gets an action by ID */
|
|
12
|
+
getTransaction(actionId: ActionId): Promise<Transform | undefined>;
|
|
13
|
+
|
|
14
|
+
/** Gets a pending action by ID */
|
|
15
|
+
getPendingTransaction(actionId: ActionId): Promise<Transform | undefined>;
|
|
16
|
+
|
|
17
|
+
/** Lists all pending action IDs */
|
|
18
|
+
listPendingTransactions(): AsyncIterable<ActionId>;
|
|
19
|
+
|
|
20
|
+
/** Saves a pending action */
|
|
21
|
+
savePendingTransaction(actionId: ActionId, transform: Transform): Promise<void>;
|
|
22
|
+
|
|
23
|
+
/** Deletes a pending action */
|
|
24
|
+
deletePendingTransaction(actionId: ActionId): Promise<void>;
|
|
25
|
+
|
|
26
|
+
/** Lists revisions in ascending or descending order between startRev and endRev (inclusive) */
|
|
27
|
+
listRevisions(startRev: number, endRev: number): AsyncIterable<ActionRev>;
|
|
28
|
+
|
|
29
|
+
/** Saves a materialized block */
|
|
30
|
+
saveMaterializedBlock(actionId: ActionId, block: IBlock | undefined): Promise<void>;
|
|
31
|
+
|
|
32
|
+
/** Saves a revision */
|
|
33
|
+
saveRevision(rev: number, actionId: ActionId): Promise<void>;
|
|
34
|
+
|
|
35
|
+
/** Promotes a pending action to committed */
|
|
36
|
+
promotePendingTransaction(actionId: ActionId): Promise<void>;
|
|
37
|
+
|
|
38
|
+
/** Sets the latest revision information */
|
|
39
|
+
setLatest(latest: ActionRev): Promise<void>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { BlockId, ActionId, ActionRev, Transform, IBlock } from "@optimystic/db-core";
|
|
2
|
+
import type { BlockMetadata } from "./struct.js";
|
|
3
|
+
|
|
4
|
+
export interface IRawStorage {
|
|
5
|
+
// Metadata operations
|
|
6
|
+
getMetadata(blockId: BlockId): Promise<BlockMetadata | undefined>;
|
|
7
|
+
saveMetadata(blockId: BlockId, metadata: BlockMetadata): Promise<void>;
|
|
8
|
+
|
|
9
|
+
// Revision operations
|
|
10
|
+
getRevision(blockId: BlockId, rev: number): Promise<ActionId | undefined>;
|
|
11
|
+
saveRevision(blockId: BlockId, rev: number, actionId: ActionId): Promise<void>;
|
|
12
|
+
/** List revisions in ascending or descending order, depending on startRev and endRev - startRev and endRev are inclusive */
|
|
13
|
+
listRevisions(blockId: BlockId, startRev: number, endRev: number): AsyncIterable<ActionRev>;
|
|
14
|
+
|
|
15
|
+
// Action operations
|
|
16
|
+
getPendingTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined>;
|
|
17
|
+
savePendingTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void>;
|
|
18
|
+
deletePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void>;
|
|
19
|
+
listPendingTransactions(blockId: BlockId): AsyncIterable<ActionId>;
|
|
20
|
+
|
|
21
|
+
getTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined>;
|
|
22
|
+
saveTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void>;
|
|
23
|
+
|
|
24
|
+
// Block materialization operations
|
|
25
|
+
getMaterializedBlock(blockId: BlockId, actionId: ActionId): Promise<IBlock | undefined>;
|
|
26
|
+
saveMaterializedBlock(blockId: BlockId, actionId: ActionId, block?: IBlock): Promise<void>;
|
|
27
|
+
|
|
28
|
+
// Promote a pending action to a committed action
|
|
29
|
+
promotePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { BlockId, IBlock, Transform, ActionId, ActionRev } from "@optimystic/db-core";
|
|
2
|
+
import type { BlockMetadata } from "./struct.js";
|
|
3
|
+
import type { IRawStorage } from "./i-raw-storage.js";
|
|
4
|
+
|
|
5
|
+
export class MemoryRawStorage implements IRawStorage {
|
|
6
|
+
private metadata = new Map<BlockId, BlockMetadata>();
|
|
7
|
+
private revisions = new Map<string, ActionId>(); // blockId:rev -> actionId
|
|
8
|
+
private pendingActions = new Map<string, Transform>(); // blockId:actionId -> transform
|
|
9
|
+
private actions = new Map<string, Transform>(); // blockId:actionId -> transform
|
|
10
|
+
private materializedBlocks = new Map<string, IBlock>(); // blockId:actionId -> block
|
|
11
|
+
|
|
12
|
+
private getRevisionKey(blockId: BlockId, rev: number): string {
|
|
13
|
+
return `${blockId}:${rev}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private getActionKey(blockId: BlockId, actionId: ActionId): string {
|
|
17
|
+
return `${blockId}:${actionId}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async getMetadata(blockId: BlockId): Promise<BlockMetadata | undefined> {
|
|
21
|
+
return this.metadata.get(blockId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async saveMetadata(blockId: BlockId, metadata: BlockMetadata): Promise<void> {
|
|
25
|
+
this.metadata.set(blockId, metadata);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async getRevision(blockId: BlockId, rev: number): Promise<ActionId | undefined> {
|
|
29
|
+
return this.revisions.get(this.getRevisionKey(blockId, rev));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async saveRevision(blockId: BlockId, rev: number, actionId: ActionId): Promise<void> {
|
|
33
|
+
this.revisions.set(this.getRevisionKey(blockId, rev), actionId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async *listRevisions(blockId: BlockId, startRev: number, endRev: number): AsyncIterable<ActionRev> {
|
|
37
|
+
const ascending = startRev <= endRev;
|
|
38
|
+
const actualStart = ascending ? startRev : endRev;
|
|
39
|
+
const actualEnd = ascending ? endRev : startRev;
|
|
40
|
+
|
|
41
|
+
const results: ActionRev[] = [];
|
|
42
|
+
for (let rev = actualStart; rev <= actualEnd; rev++) {
|
|
43
|
+
const actionId = this.revisions.get(this.getRevisionKey(blockId, rev));
|
|
44
|
+
if (actionId) {
|
|
45
|
+
results.push({ rev, actionId });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!ascending) {
|
|
50
|
+
results.reverse();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const result of results) {
|
|
54
|
+
yield result;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getPendingTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
|
|
59
|
+
return this.pendingActions.get(this.getActionKey(blockId, actionId));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async savePendingTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
|
|
63
|
+
this.pendingActions.set(this.getActionKey(blockId, actionId), transform);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async deletePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
67
|
+
this.pendingActions.delete(this.getActionKey(blockId, actionId));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async *listPendingTransactions(blockId: BlockId): AsyncIterable<ActionId> {
|
|
71
|
+
const prefix = `${blockId}:`;
|
|
72
|
+
for (const [key] of Array.from(this.pendingActions.entries())) {
|
|
73
|
+
if (key.startsWith(prefix)) {
|
|
74
|
+
yield key.substring(prefix.length);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
|
|
80
|
+
return this.actions.get(this.getActionKey(blockId, actionId));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async saveTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
|
|
84
|
+
this.actions.set(this.getActionKey(blockId, actionId), transform);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getMaterializedBlock(blockId: BlockId, actionId: ActionId): Promise<IBlock | undefined> {
|
|
88
|
+
return this.materializedBlocks.get(this.getActionKey(blockId, actionId));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async saveMaterializedBlock(blockId: BlockId, actionId: ActionId, block?: IBlock): Promise<void> {
|
|
92
|
+
const key = this.getActionKey(blockId, actionId);
|
|
93
|
+
if (block) {
|
|
94
|
+
this.materializedBlocks.set(key, block);
|
|
95
|
+
} else {
|
|
96
|
+
this.materializedBlocks.delete(key);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async promotePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
101
|
+
const key = this.getActionKey(blockId, actionId);
|
|
102
|
+
const transform = this.pendingActions.get(key);
|
|
103
|
+
if (transform) {
|
|
104
|
+
this.actions.set(key, transform);
|
|
105
|
+
this.pendingActions.delete(key);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|