@pdsjs/spaces 1.0.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/package.json +40 -0
- package/src/authority.d.ts +47 -0
- package/src/authority.js +148 -0
- package/src/blake3.d.ts +14 -0
- package/src/blake3.js +272 -0
- package/src/car.d.ts +41 -0
- package/src/car.js +98 -0
- package/src/commit.d.ts +109 -0
- package/src/commit.js +152 -0
- package/src/handlers/auth.d.ts +67 -0
- package/src/handlers/auth.js +436 -0
- package/src/handlers/manage.d.ts +20 -0
- package/src/handlers/manage.js +508 -0
- package/src/handlers/read.d.ts +18 -0
- package/src/handlers/read.js +394 -0
- package/src/handlers/write.d.ts +18 -0
- package/src/handlers/write.js +462 -0
- package/src/index.d.ts +15 -0
- package/src/index.js +56 -0
- package/src/lthash.d.ts +24 -0
- package/src/lthash.js +86 -0
- package/src/mac.d.ts +50 -0
- package/src/mac.js +107 -0
- package/src/memory-storage.d.ts +4 -0
- package/src/memory-storage.js +244 -0
- package/src/path.d.ts +22 -0
- package/src/path.js +33 -0
- package/src/routes.d.ts +23 -0
- package/src/routes.js +42 -0
- package/src/service-auth.d.ts +42 -0
- package/src/service-auth.js +153 -0
- package/src/space-row.d.ts +15 -0
- package/src/space-row.js +33 -0
- package/src/token.d.ts +95 -0
- package/src/token.js +223 -0
- package/src/uri.d.ts +32 -0
- package/src/uri.js +89 -0
- package/src/verifier.d.ts +15 -0
- package/src/verifier.js +74 -0
- package/src/writer.d.ts +77 -0
- package/src/writer.js +195 -0
package/src/writer.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// @pdsjs/spaces/writer - the single write path for permissioned repos.
|
|
2
|
+
//
|
|
3
|
+
// createRecord, putRecord, deleteRecord and applyWrites all funnel through
|
|
4
|
+
// applyWrites here: one commit, one rev, one LtHash advance, one atomic
|
|
5
|
+
// commitSpaceWrite. Nothing else mutates a permissioned repo.
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
cborEncodeDagCbor,
|
|
9
|
+
cidToString,
|
|
10
|
+
createCid,
|
|
11
|
+
createTid,
|
|
12
|
+
} from '@pdsjs/core/repo';
|
|
13
|
+
import { RepoCommit } from './commit.js';
|
|
14
|
+
import { formatRecordPath } from './path.js';
|
|
15
|
+
import { makeSpaceRow } from './space-row.js';
|
|
16
|
+
|
|
17
|
+
export const MAX_WRITES_PER_COMMIT = 200;
|
|
18
|
+
|
|
19
|
+
export class SpaceWriteError extends Error {
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} message
|
|
22
|
+
* @param {string} code - machine-readable error name for the XRPC response
|
|
23
|
+
*/
|
|
24
|
+
constructor(message, code) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'SpaceWriteError';
|
|
27
|
+
this.code = code;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class SpaceRecordNotFoundError extends SpaceWriteError {
|
|
32
|
+
/** @param {string} collection @param {string} rkey */
|
|
33
|
+
constructor(collection, rkey) {
|
|
34
|
+
super(`Record not found: ${collection}/${rkey}`, 'RecordNotFound');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class SpaceRecordAlreadyExistsError extends SpaceWriteError {
|
|
39
|
+
/** @param {string} collection @param {string} rkey */
|
|
40
|
+
constructor(collection, rkey) {
|
|
41
|
+
super(
|
|
42
|
+
`Record already exists: ${collection}/${rkey}`,
|
|
43
|
+
'RecordAlreadyExists',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Commits for one space must not interleave: applyWrites reads the current set
|
|
49
|
+
// hash and the touched records' CIDs, then commits a state derived from them.
|
|
50
|
+
// Two concurrent commits would each fold their ops into the same starting hash
|
|
51
|
+
// and the last write would drop the other's contribution. The port's commit is
|
|
52
|
+
// atomic but cannot serialise across calls, so serialise here — sound because a
|
|
53
|
+
// permissioned repo lives in exactly one process (a Node PDS, or one Durable
|
|
54
|
+
// Object).
|
|
55
|
+
/** @type {WeakMap<object, Map<string, Promise<any>>>} */
|
|
56
|
+
const spaceQueues = new WeakMap();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @template T
|
|
60
|
+
* @param {object} storage
|
|
61
|
+
* @param {string} space
|
|
62
|
+
* @param {() => Promise<T>} fn
|
|
63
|
+
* @returns {Promise<T>}
|
|
64
|
+
*/
|
|
65
|
+
function serialize(storage, space, fn) {
|
|
66
|
+
let queues = spaceQueues.get(storage);
|
|
67
|
+
if (!queues) {
|
|
68
|
+
queues = new Map();
|
|
69
|
+
spaceQueues.set(storage, queues);
|
|
70
|
+
}
|
|
71
|
+
const prior = queues.get(space) ?? Promise.resolve();
|
|
72
|
+
// Chain off the prior commit's settlement, not its value, so one failed
|
|
73
|
+
// commit does not poison every later write to the same space.
|
|
74
|
+
const next = prior.then(fn, fn);
|
|
75
|
+
queues.set(
|
|
76
|
+
space,
|
|
77
|
+
next.then(
|
|
78
|
+
() => undefined,
|
|
79
|
+
() => undefined,
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
return next;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @typedef {Object} SpaceWriteInput
|
|
87
|
+
* @property {'create'|'update'|'put'|'delete'} action
|
|
88
|
+
* @property {string} collection
|
|
89
|
+
* @property {string} rkey
|
|
90
|
+
* @property {Object} [record] - required for everything but delete
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @typedef {Object} SpaceWriteResult
|
|
95
|
+
* @property {'create'|'update'|'delete'} action
|
|
96
|
+
* @property {string} collection
|
|
97
|
+
* @property {string} rkey
|
|
98
|
+
* @property {string|null} cid - null for deletes
|
|
99
|
+
* @property {string|null} prev - null for creates
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Apply a batch of writes to a permissioned repo as one commit.
|
|
104
|
+
*
|
|
105
|
+
* Writes resolve in order against the state the batch has built up, not just
|
|
106
|
+
* what was stored on entry, so a batch can create then update a record and a
|
|
107
|
+
* repeated create is still caught rather than double-counted in the set hash.
|
|
108
|
+
*
|
|
109
|
+
* @param {import('@pdsjs/core/ports').SpaceStoragePort} storage
|
|
110
|
+
* @param {Object} opts
|
|
111
|
+
* @param {string} opts.space - space AT-URI
|
|
112
|
+
* @param {SpaceWriteInput[]} opts.writes
|
|
113
|
+
* @param {string} [opts.now] - ISO timestamp, for deterministic tests
|
|
114
|
+
* @returns {Promise<{rev: string, setHash: Uint8Array, results: SpaceWriteResult[]}>}
|
|
115
|
+
*/
|
|
116
|
+
export async function applyWrites(storage, { space, writes, now }) {
|
|
117
|
+
if (writes.length > MAX_WRITES_PER_COMMIT) {
|
|
118
|
+
throw new SpaceWriteError(
|
|
119
|
+
`Too many writes. Max: ${MAX_WRITES_PER_COMMIT}`,
|
|
120
|
+
'InvalidRequest',
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return serialize(storage, space, async () => {
|
|
125
|
+
const timestamp = now ?? new Date().toISOString();
|
|
126
|
+
|
|
127
|
+
// A writer's PDS is never told about membership — the member list is the
|
|
128
|
+
// authority's concern — so it materialises its own repo on first write.
|
|
129
|
+
if ((await storage.getSpace(space)) === null) {
|
|
130
|
+
await storage.putSpace(
|
|
131
|
+
makeSpaceRow(space, { isOwner: false, createdAt: timestamp }),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const state = await storage.getSpaceRepo(space);
|
|
136
|
+
const repo = RepoCommit.fromState(state?.setHash ?? null);
|
|
137
|
+
const rev = createTid();
|
|
138
|
+
|
|
139
|
+
/** @type {Map<string, string|null>} live cid per touched path; null = deleted */
|
|
140
|
+
const staged = new Map();
|
|
141
|
+
/** @type {SpaceWriteResult[]} */
|
|
142
|
+
const results = [];
|
|
143
|
+
/** @type {import('@pdsjs/core/ports').SpaceWrite[]} */
|
|
144
|
+
const portWrites = [];
|
|
145
|
+
|
|
146
|
+
for (const write of writes) {
|
|
147
|
+
const { collection, rkey } = write;
|
|
148
|
+
const key = formatRecordPath(collection, rkey);
|
|
149
|
+
const prev = staged.has(key)
|
|
150
|
+
? /** @type {string|null} */ (staged.get(key))
|
|
151
|
+
: await storage.getSpaceRecordCid(space, collection, rkey);
|
|
152
|
+
|
|
153
|
+
if (write.action === 'create' && prev) {
|
|
154
|
+
throw new SpaceRecordAlreadyExistsError(collection, rkey);
|
|
155
|
+
}
|
|
156
|
+
if ((write.action === 'update' || write.action === 'delete') && !prev) {
|
|
157
|
+
throw new SpaceRecordNotFoundError(collection, rkey);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (write.action === 'delete') {
|
|
161
|
+
repo.applyOp({ collection, rkey, cid: null, prev });
|
|
162
|
+
staged.set(key, null);
|
|
163
|
+
results.push({ action: 'delete', collection, rkey, cid: null, prev });
|
|
164
|
+
portWrites.push({
|
|
165
|
+
action: 'delete',
|
|
166
|
+
collection,
|
|
167
|
+
rkey,
|
|
168
|
+
cid: null,
|
|
169
|
+
value: null,
|
|
170
|
+
prev,
|
|
171
|
+
});
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const value = cborEncodeDagCbor(write.record);
|
|
176
|
+
const cid = cidToString(await createCid(value));
|
|
177
|
+
repo.applyOp({ collection, rkey, cid, prev });
|
|
178
|
+
staged.set(key, cid);
|
|
179
|
+
|
|
180
|
+
const action = prev ? 'update' : 'create';
|
|
181
|
+
results.push({ action, collection, rkey, cid, prev });
|
|
182
|
+
portWrites.push({ action, collection, rkey, cid, value, prev });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const setHash = repo.state();
|
|
186
|
+
await storage.commitSpaceWrite(space, {
|
|
187
|
+
writes: portWrites,
|
|
188
|
+
setHash,
|
|
189
|
+
rev,
|
|
190
|
+
indexedAt: timestamp,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return { rev, setHash, results };
|
|
194
|
+
});
|
|
195
|
+
}
|