@atproto/repo 0.10.2 → 0.10.3
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/CHANGELOG.md +16 -0
- package/package.json +22 -17
- package/jest.config.cjs +0 -24
- package/src/block-map.ts +0 -131
- package/src/car.ts +0 -357
- package/src/cid-set.ts +0 -55
- package/src/data-diff.ts +0 -117
- package/src/error.ts +0 -43
- package/src/index.ts +0 -11
- package/src/logger.ts +0 -7
- package/src/mst/diff.ts +0 -114
- package/src/mst/index.ts +0 -4
- package/src/mst/mst.ts +0 -892
- package/src/mst/util.ts +0 -160
- package/src/mst/walker.ts +0 -118
- package/src/parse.ts +0 -44
- package/src/readable-repo.ts +0 -86
- package/src/repo.ts +0 -236
- package/src/storage/index.ts +0 -4
- package/src/storage/memory-blockstore.ts +0 -76
- package/src/storage/readable-blockstore.ts +0 -55
- package/src/storage/sync-storage.ts +0 -35
- package/src/storage/types.ts +0 -47
- package/src/sync/consumer.ts +0 -207
- package/src/sync/index.ts +0 -2
- package/src/sync/provider.ts +0 -67
- package/src/types.ts +0 -227
- package/src/util.ts +0 -146
- package/tests/_keys.ts +0 -156
- package/tests/_util.ts +0 -265
- package/tests/car-file-fixtures.json +0 -28
- package/tests/car.test.ts +0 -125
- package/tests/commit-data.test.ts +0 -94
- package/tests/commit-proof-fixtures.json +0 -118
- package/tests/commit-proofs.test.ts +0 -63
- package/tests/covering-proofs.test.ts +0 -256
- package/tests/mst.test.ts +0 -450
- package/tests/proofs.test.ts +0 -155
- package/tests/repo.test.ts +0 -106
- package/tests/sync.test.ts +0 -95
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -7
package/tests/_util.ts
DELETED
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import { TID } from '@atproto/common-web'
|
|
3
|
-
import * as crypto from '@atproto/crypto'
|
|
4
|
-
import { Keypair, randomBytes } from '@atproto/crypto'
|
|
5
|
-
import * as cbor from '@atproto/lex-cbor'
|
|
6
|
-
import { Cid, cidForCbor, parseCid } from '@atproto/lex-data'
|
|
7
|
-
import { NsidString } from '@atproto/syntax'
|
|
8
|
-
import {
|
|
9
|
-
BlockMap,
|
|
10
|
-
CollectionContents,
|
|
11
|
-
Commit,
|
|
12
|
-
CommitData,
|
|
13
|
-
DataDiff,
|
|
14
|
-
RecordPath,
|
|
15
|
-
RecordWriteOp,
|
|
16
|
-
RepoContents,
|
|
17
|
-
WriteOpAction,
|
|
18
|
-
} from '../src/index.js'
|
|
19
|
-
import { MST } from '../src/mst/index.js'
|
|
20
|
-
import { Repo } from '../src/repo.js'
|
|
21
|
-
import { RepoStorage } from '../src/storage/index.js'
|
|
22
|
-
|
|
23
|
-
type IdMapping = Record<string, Cid>
|
|
24
|
-
|
|
25
|
-
export const randomCid = async (storage?: RepoStorage): Promise<Cid> => {
|
|
26
|
-
const bytes = cbor.encode({ test: randomStr(50) })
|
|
27
|
-
const cid = await cidForCbor(bytes)
|
|
28
|
-
if (storage) {
|
|
29
|
-
// @ts-expect-error FIXME remove this comment (and fix the TS error)
|
|
30
|
-
await storage.putBlock(cid, bytes)
|
|
31
|
-
}
|
|
32
|
-
return cid
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const generateBulkDataKeys = async (
|
|
36
|
-
count: number,
|
|
37
|
-
blockstore?: RepoStorage,
|
|
38
|
-
): Promise<IdMapping> => {
|
|
39
|
-
const obj: IdMapping = {}
|
|
40
|
-
for (let i = 0; i < count; i++) {
|
|
41
|
-
const key = `com.example.record/${TID.nextStr()}`
|
|
42
|
-
obj[key] = await randomCid(blockstore)
|
|
43
|
-
}
|
|
44
|
-
return obj
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const keysFromMapping = (mapping: IdMapping): TID[] => {
|
|
48
|
-
return Object.keys(mapping).map((id) => TID.fromStr(id))
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export const keysFromMappings = (mappings: IdMapping[]): TID[] => {
|
|
52
|
-
return mappings.map(keysFromMapping).flat()
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export const randomStr = (len: number): string => {
|
|
56
|
-
let result = ''
|
|
57
|
-
const CHARS = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
58
|
-
for (let i = 0; i < len; i++) {
|
|
59
|
-
result += CHARS.charAt(Math.floor(Math.random() * CHARS.length))
|
|
60
|
-
}
|
|
61
|
-
return result
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export const shuffle = <T>(arr: T[]): T[] => {
|
|
65
|
-
const toShuffle = [...arr]
|
|
66
|
-
const shuffled: T[] = []
|
|
67
|
-
while (toShuffle.length > 0) {
|
|
68
|
-
const index = Math.floor(Math.random() * toShuffle.length)
|
|
69
|
-
shuffled.push(toShuffle[index])
|
|
70
|
-
toShuffle.splice(index, 1)
|
|
71
|
-
}
|
|
72
|
-
return shuffled
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export const generateObject = (): Record<string, string> => {
|
|
76
|
-
return {
|
|
77
|
-
name: randomStr(100),
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Mass repo mutations & checking
|
|
82
|
-
// -------------------------------
|
|
83
|
-
|
|
84
|
-
export const testCollections: NsidString[] = [
|
|
85
|
-
'com.example.posts',
|
|
86
|
-
'com.example.likes',
|
|
87
|
-
]
|
|
88
|
-
|
|
89
|
-
export const fillRepo = async (
|
|
90
|
-
repo: Repo,
|
|
91
|
-
keypair: crypto.Keypair,
|
|
92
|
-
itemsPerCollection: number,
|
|
93
|
-
): Promise<{ repo: Repo; data: RepoContents }> => {
|
|
94
|
-
const repoData: RepoContents = {}
|
|
95
|
-
const writes: RecordWriteOp[] = []
|
|
96
|
-
for (const collName of testCollections) {
|
|
97
|
-
const collData: CollectionContents = {}
|
|
98
|
-
for (let i = 0; i < itemsPerCollection; i++) {
|
|
99
|
-
const object = generateObject()
|
|
100
|
-
const rkey = TID.nextStr()
|
|
101
|
-
collData[rkey] = object
|
|
102
|
-
writes.push({
|
|
103
|
-
action: WriteOpAction.Create,
|
|
104
|
-
collection: collName,
|
|
105
|
-
rkey,
|
|
106
|
-
record: object,
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
repoData[collName] = collData
|
|
110
|
-
}
|
|
111
|
-
const updated = await repo.applyWrites(writes, keypair)
|
|
112
|
-
return {
|
|
113
|
-
repo: updated,
|
|
114
|
-
data: repoData,
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export const formatEdit = async (
|
|
119
|
-
repo: Repo,
|
|
120
|
-
prevData: RepoContents,
|
|
121
|
-
keypair: crypto.Keypair,
|
|
122
|
-
params: {
|
|
123
|
-
adds?: number
|
|
124
|
-
updates?: number
|
|
125
|
-
deletes?: number
|
|
126
|
-
},
|
|
127
|
-
): Promise<{ commit: CommitData; data: RepoContents }> => {
|
|
128
|
-
const { adds = 0, updates = 0, deletes = 0 } = params
|
|
129
|
-
const repoData: RepoContents = {}
|
|
130
|
-
const writes: RecordWriteOp[] = []
|
|
131
|
-
for (const collName of testCollections) {
|
|
132
|
-
const collData = { ...(prevData[collName] ?? {}) }
|
|
133
|
-
const shuffled = shuffle(Object.entries(collData))
|
|
134
|
-
|
|
135
|
-
for (let i = 0; i < adds; i++) {
|
|
136
|
-
const object = generateObject()
|
|
137
|
-
const rkey = TID.nextStr()
|
|
138
|
-
collData[rkey] = object
|
|
139
|
-
writes.push({
|
|
140
|
-
action: WriteOpAction.Create,
|
|
141
|
-
collection: collName,
|
|
142
|
-
rkey,
|
|
143
|
-
record: object,
|
|
144
|
-
})
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const toUpdate = shuffled.slice(0, updates)
|
|
148
|
-
for (let i = 0; i < toUpdate.length; i++) {
|
|
149
|
-
const object = generateObject()
|
|
150
|
-
const rkey = toUpdate[i][0]
|
|
151
|
-
collData[rkey] = object
|
|
152
|
-
writes.push({
|
|
153
|
-
action: WriteOpAction.Update,
|
|
154
|
-
collection: collName,
|
|
155
|
-
rkey,
|
|
156
|
-
record: object,
|
|
157
|
-
})
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const toDelete = shuffled.slice(updates, deletes)
|
|
161
|
-
for (let i = 0; i < toDelete.length; i++) {
|
|
162
|
-
const rkey = toDelete[i][0]
|
|
163
|
-
delete collData[rkey]
|
|
164
|
-
writes.push({
|
|
165
|
-
action: WriteOpAction.Delete,
|
|
166
|
-
collection: collName,
|
|
167
|
-
rkey,
|
|
168
|
-
})
|
|
169
|
-
}
|
|
170
|
-
repoData[collName] = collData
|
|
171
|
-
}
|
|
172
|
-
const commit = await repo.formatCommit(writes, keypair)
|
|
173
|
-
return {
|
|
174
|
-
commit,
|
|
175
|
-
data: repoData,
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export const pathsForOps = (ops: RecordWriteOp[]): RecordPath[] =>
|
|
180
|
-
ops.map((op) => ({ collection: op.collection, rkey: op.rkey }))
|
|
181
|
-
|
|
182
|
-
export const saveMst = async (storage: RepoStorage, mst: MST): Promise<Cid> => {
|
|
183
|
-
const diff = await mst.getUnstoredBlocks()
|
|
184
|
-
// @ts-expect-error FIXME remove this comment (and fix the TS error)
|
|
185
|
-
await storage.putMany(diff.blocks)
|
|
186
|
-
return diff.root
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Creating repo
|
|
190
|
-
// -------------------
|
|
191
|
-
export const addBadCommit = async (
|
|
192
|
-
repo: Repo,
|
|
193
|
-
keypair: Keypair,
|
|
194
|
-
): Promise<Repo> => {
|
|
195
|
-
const obj = generateObject()
|
|
196
|
-
const newBlocks = new BlockMap()
|
|
197
|
-
const cid = await newBlocks.add(obj)
|
|
198
|
-
const updatedData = await repo.data.add(`com.example.test/${TID.next()}`, cid)
|
|
199
|
-
const dataCid = await updatedData.getPointer()
|
|
200
|
-
const diff = await DataDiff.of(updatedData, repo.data)
|
|
201
|
-
newBlocks.addMap(diff.newMstBlocks)
|
|
202
|
-
// we generate a bad sig by signing some other data
|
|
203
|
-
const rev = TID.nextStr(repo.commit.rev)
|
|
204
|
-
const commit: Commit = {
|
|
205
|
-
...repo.commit,
|
|
206
|
-
rev,
|
|
207
|
-
data: dataCid,
|
|
208
|
-
sig: await keypair.sign(randomBytes(256)),
|
|
209
|
-
}
|
|
210
|
-
const commitCid = await newBlocks.add(commit)
|
|
211
|
-
|
|
212
|
-
// @ts-expect-error FIXME remove this comment (and fix the TS error)
|
|
213
|
-
await repo.storage.applyCommit({
|
|
214
|
-
cid: commitCid,
|
|
215
|
-
rev,
|
|
216
|
-
prev: repo.cid,
|
|
217
|
-
newBlocks,
|
|
218
|
-
removedCids: diff.removedCids,
|
|
219
|
-
})
|
|
220
|
-
return await Repo.load(repo.storage, commitCid)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Logging
|
|
224
|
-
// ----------------
|
|
225
|
-
|
|
226
|
-
export const writeMstLog = async (filename: string, tree: MST) => {
|
|
227
|
-
let log = ''
|
|
228
|
-
for await (const entry of tree.walk()) {
|
|
229
|
-
if (entry.isLeaf()) continue
|
|
230
|
-
const layer = await entry.getLayer()
|
|
231
|
-
log += `Layer ${layer}: ${entry.pointer}\n`
|
|
232
|
-
log += '--------------\n'
|
|
233
|
-
const entries = await entry.getEntries()
|
|
234
|
-
for (const e of entries) {
|
|
235
|
-
if (e.isLeaf()) {
|
|
236
|
-
log += `Key: ${e.key} (${e.value})\n`
|
|
237
|
-
} else {
|
|
238
|
-
log += `Subtree: ${e.pointer}\n`
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
log += '\n\n'
|
|
242
|
-
}
|
|
243
|
-
fs.writeFileSync(filename, log)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export const saveMstEntries = (filename: string, entries: [string, Cid][]) => {
|
|
247
|
-
const writable = entries.map(([key, val]) => [key, val.toString()])
|
|
248
|
-
fs.writeFileSync(filename, JSON.stringify(writable))
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export const loadMstEntries = (filename: string): [string, Cid][] => {
|
|
252
|
-
const contents = fs.readFileSync(filename)
|
|
253
|
-
const parsed = JSON.parse(contents.toString())
|
|
254
|
-
return parsed.map(([key, value]) => [key, parseCid(value)])
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export async function toBuffer(
|
|
258
|
-
stream: AsyncIterable<Uint8Array> | Iterable<Uint8Array>,
|
|
259
|
-
): Promise<Buffer> {
|
|
260
|
-
const chunks: Uint8Array[] = []
|
|
261
|
-
for await (const chunk of stream) {
|
|
262
|
-
chunks.push(chunk)
|
|
263
|
-
}
|
|
264
|
-
return Buffer.concat(chunks)
|
|
265
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"root": "bafyreiapldaco7m23c7qzc4w42r7kxmcswm64nkindtuh4vwztrpoe7m5m",
|
|
4
|
-
"blocks": [
|
|
5
|
-
{
|
|
6
|
-
"cid": "bafyreiapldaco7m23c7qzc4w42r7kxmcswm64nkindtuh4vwztrpoe7m5m",
|
|
7
|
-
"bytes": "oWR0ZXN0ZHJvb3Q"
|
|
8
|
-
},
|
|
9
|
-
{
|
|
10
|
-
"cid": "bafyreieteuyxvbvjbvuhsuo54qx6r3tnjxtp3ub6kb66rdjml3murxjcsy",
|
|
11
|
-
"bytes": "oWR0ZXN0WQEAM32TVA//xgHS9Gtukp0vw6whQ+TnlwF9czt5A7Dxz/URSbryc9Pdw7HESX+jC2oPI/6rwKbhSml2kxJo4MaUeIg/HWI9ixxALw5gIF/I0JC3ejXVAu1Pw6bI9RWa5TgIvAnSow0pJ6jbaaWHlxCpqqHCNHUYbIC14D9k+RK3yS0h2g+O+gRUETQt8t4jOKxEhD037cYEuJCD+fWzFoLkEkrPdUWeqlFQxGt6bflCYjZFgiZKFUo72afR3XM16+jOlhOl+EtuqFcijYJ6MIB53qI8P8HMC2RVH0Mv8UYWLcWatl+CNLykEjesnMar5CvZT8j4w5EyEiS09iD4r6bljg"
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
"cid": "bafyreiahvdwcywzm35id7hajal5l73p4bnoyaowgckeatd6sbxsacoo5jq",
|
|
15
|
-
"bytes": "oWR0ZXN0WID81CSnkdKi0OBexCStL5gex6Fkehtg7R9Jaww7LFuJC/6Or9Cdb58I+6T9Kjqhf1bf5t5WLbTGt8HOlf1Ysl3wqfdxdxnRZYjyng8YjEmH2Twkc//moluskzywxfOwhRXsXI7SYt36OUkS8AKDzmhTijOG2XWSa2QvU3iSSjP8zw"
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
"cid": "bafyreictawwxwjuto6csptbtktbbgwkrryqoakgyb3qovakgiqjzmvzi6a",
|
|
19
|
-
"bytes": "oWR0ZXN0RBnEJSE"
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
"cid": "bafyreierfkr6cwv2ux5hk6hp5qh5fhgzyr4yicad5m3pyu3ndaatyqucxu",
|
|
23
|
-
"bytes": "oWR0ZXN0WQH09xLXHen1UTIPEap18egWK6OiRVQ4oLBqjfBVQGdiSsmsIn8uXDM6wp63kCrYWLvXoI0dREVW6+RUjoQIbmDhiKbEUTuLbcFiJnwD5gSlkXdUEwO4UeLfvcAsDJXel5lLaOlOeRTfA3Wf+rEEpr/ccCuwOATBPZBjPrneFVQu8UdsvTu5W144tGxp/ptBsMBqM3yLYkYYHNRrjcAI7iFiszsfKzo28GyM199Jko11mcVhNQ8KHZS72jbsWW/HyfJsL1M/dn6sDCfIaro66mTLRddSQaheQL4NBW5FEgLUBO2VDuBT9fVIl2IwQcijZAOakjLkS1sY2SyUmdsicqGRalnOrVlC5iWQcwHDXLzm9GWz/vfuoF+jzWsdpo6cjT5MIN8uXpzoZRSjH1+UXFxCzFhXkDwL/xJUq8u/0OFGp0mzDc7RLO3gC7X/ENaVPtz/wQaZ3q00EeHvDuiaxHdKIesf/+IkbqS1XjKtaHemB511MFiVf7l2OcqsUU12A5VMreqWPwbAHLgFRDPWiLS0D7yEF3KJX1IupxBmidQMT+SlmCp7FZMdJeHJ3pzpbQv+EoKSXja7it2D2uYsMcTn2DGhlVYsCcQd8PVNKZmPXuC7D82N4sh8p2XVW4LbsDfNTOrgHLX7zRX31VNa47w5Uc03Wuk"
|
|
24
|
-
}
|
|
25
|
-
],
|
|
26
|
-
"car": "OqJlcm9vdHOB2CpYJQABcRIgD1jAJ32a2L8Mi5bmo/VdgpWZ7jVIaOdD8rbM4vcT7OtndmVyc2lvbgEvAXESIA9YwCd9mti/DIuW5qP1XYKVme41SGjnQ/K2zOL3E+zroWR0ZXN0ZHJvb3StAgFxEiCTJTF6hqkNaHlR3eQv6O5tTeb90D5QfeiNLF7ZSN0ilqFkdGVzdFkBADN9k1QP/8YB0vRrbpKdL8OsIUPk55cBfXM7eQOw8c/1EUm68nPT3cOxxEl/owtqDyP+q8Cm4UppdpMSaODGlHiIPx1iPYscQC8OYCBfyNCQt3o11QLtT8OmyPUVmuU4CLwJ0qMNKSeo22mlh5cQqaqhwjR1GGyAteA/ZPkSt8ktIdoPjvoEVBE0LfLeIzisRIQ9N+3GBLiQg/n1sxaC5BJKz3VFnqpRUMRrem35QmI2RYImShVKO9mn0d1zNevozpYTpfhLbqhXIo2CejCAed6iPD/BzAtkVR9DL/FGFi3FmrZfgjS8pBI3rJzGq+Qr2U/I+MORMhIktPYg+K+m5Y6sAQFxEiAHqOwsWyzfUD+cCQL6v+38C12AOsYSiAmP0g3kATndTKFkdGVzdFiA/NQkp5HSotDgXsQkrS+YHsehZHobYO0fSWsMOyxbiQv+jq/QnW+fCPuk/So6oX9W3+beVi20xrfBzpX9WLJd8Kn3cXcZ0WWI8p4PGIxJh9k8JHP/5qJbrJM8sMXzsIUV7FyO0mLd+jlJEvACg85oU4ozhtl1kmtkL1N4kkoz/M8vAXESIFMFrXsmk3eFJ8wzVMITWVGOIOAo2A7g6oFGRBOWVyjwoWR0ZXN0RBnEJSGhBAFxEiCRKqPhWrql+nV47+wP0pzZxHmECAPrNvxTbRgBPEKCvaFkdGVzdFkB9PcS1x3p9VEyDxGqdfHoFiujokVUOKCwao3wVUBnYkrJrCJ/LlwzOsKet5Aq2Fi716CNHURFVuvkVI6ECG5g4YimxFE7i23BYiZ8A+YEpZF3VBMDuFHi373ALAyV3peZS2jpTnkU3wN1n/qxBKa/3HArsDgEwT2QYz653hVULvFHbL07uVteOLRsaf6bQbDAajN8i2JGGBzUa43ACO4hYrM7Hys6NvBsjNffSZKNdZnFYTUPCh2Uu9o27Flvx8nybC9TP3Z+rAwnyGq6Oupky0XXUkGoXkC+DQVuRRIC1ATtlQ7gU/X1SJdiMEHIo2QDmpIy5EtbGNkslJnbInKhkWpZzq1ZQuYlkHMBw1y85vRls/737qBfo81rHaaOnI0+TCDfLl6c6GUUox9flFxcQsxYV5A8C/8SVKvLv9DhRqdJsw3O0Szt4Au1/xDWlT7c/8EGmd6tNBHh7w7omsR3SiHrH//iJG6ktV4yrWh3pgeddTBYlX+5djnKrFFNdgOVTK3qlj8GwBy4BUQz1oi0tA+8hBdyiV9SLqcQZonUDE/kpZgqexWTHSXhyd6c6W0L/hKCkl42u4rdg9rmLDHE59gxoZVWLAnEHfD1TSmZj17guw/NjeLIfKdl1VuC27A3zUzq4By1+80V99VTWuO8OVHNN1rp"
|
|
27
|
-
}
|
|
28
|
-
]
|
package/tests/car.test.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { wait } from '@atproto/common-web'
|
|
2
|
-
import { encode } from '@atproto/lex-cbor'
|
|
3
|
-
import {
|
|
4
|
-
Cid,
|
|
5
|
-
LexValue,
|
|
6
|
-
cidForCbor,
|
|
7
|
-
fromBase64,
|
|
8
|
-
parseCid,
|
|
9
|
-
toBase64,
|
|
10
|
-
} from '@atproto/lex-data'
|
|
11
|
-
import { CarBlock, readCarStream, writeCarStream } from '../src/index.js'
|
|
12
|
-
import fixtures from './car-file-fixtures.json' with { type: 'json' }
|
|
13
|
-
|
|
14
|
-
async function dataToCborBlock(data: LexValue): Promise<{
|
|
15
|
-
cid: Cid
|
|
16
|
-
bytes: Uint8Array
|
|
17
|
-
}> {
|
|
18
|
-
const bytes = encode(data)
|
|
19
|
-
const cid = await cidForCbor(bytes)
|
|
20
|
-
return { cid, bytes }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe('car', () => {
|
|
24
|
-
for (const fixture of fixtures) {
|
|
25
|
-
it('correctly writes car files', async () => {
|
|
26
|
-
const root = parseCid(fixture.root)
|
|
27
|
-
async function* blockIter() {
|
|
28
|
-
for (const block of fixture.blocks) {
|
|
29
|
-
const cid = parseCid(block.cid)
|
|
30
|
-
const bytes = fromBase64(block.bytes, 'base64')
|
|
31
|
-
yield { cid, bytes }
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
const carStream = writeCarStream(root, blockIter())
|
|
35
|
-
const chunks: Uint8Array[] = []
|
|
36
|
-
for await (const chunk of carStream) {
|
|
37
|
-
chunks.push(chunk)
|
|
38
|
-
}
|
|
39
|
-
const car = Buffer.concat(chunks)
|
|
40
|
-
// @NOTE Not using car.toString('base64') because of padding differences
|
|
41
|
-
expect(toBase64(car)).toEqual(fixture.car)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('correctly reads carfiles', async () => {
|
|
45
|
-
const carStream = [fromBase64(fixture.car, 'base64')]
|
|
46
|
-
const { roots, blocks } = await readCarStream(carStream)
|
|
47
|
-
expect(roots.length).toBe(1)
|
|
48
|
-
expect(roots[0].toString()).toEqual(fixture.root)
|
|
49
|
-
const carBlocks: CarBlock[] = []
|
|
50
|
-
for await (const block of blocks) {
|
|
51
|
-
carBlocks.push(block)
|
|
52
|
-
}
|
|
53
|
-
expect(carBlocks.length).toEqual(fixture.blocks.length)
|
|
54
|
-
for (let i = 0; i < carBlocks.length; i++) {
|
|
55
|
-
expect(carBlocks[i].cid.toString()).toEqual(fixture.blocks[i].cid)
|
|
56
|
-
expect(toBase64(carBlocks[i].bytes, 'base64')).toEqual(
|
|
57
|
-
fixture.blocks[i].bytes,
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
})
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
it('writeCar propagates errors', async () => {
|
|
64
|
-
const iterate = async () => {
|
|
65
|
-
async function* blockIterator() {
|
|
66
|
-
await wait(1)
|
|
67
|
-
const block = await dataToCborBlock({ test: 1 })
|
|
68
|
-
yield block
|
|
69
|
-
throw new Error('Oops!')
|
|
70
|
-
}
|
|
71
|
-
const iter = writeCarStream(null, blockIterator())
|
|
72
|
-
for await (const _bytes of iter) {
|
|
73
|
-
// no-op
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
await expect(iterate).rejects.toThrow('Oops!')
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('verifies CIDs', async () => {
|
|
80
|
-
const block0 = await dataToCborBlock({ block: 0 })
|
|
81
|
-
const block1 = await dataToCborBlock({ block: 1 })
|
|
82
|
-
const block2 = await dataToCborBlock({ block: 2 })
|
|
83
|
-
const block3 = await dataToCborBlock({ block: 3 })
|
|
84
|
-
const badBlock = await dataToCborBlock({ block: 'bad' })
|
|
85
|
-
const blockIter = async function* () {
|
|
86
|
-
yield block0
|
|
87
|
-
yield block1
|
|
88
|
-
yield block2
|
|
89
|
-
yield { cid: block3.cid, bytes: badBlock.bytes }
|
|
90
|
-
}
|
|
91
|
-
const flush = async function (iter: AsyncIterable<unknown>) {
|
|
92
|
-
for await (const _ of iter) {
|
|
93
|
-
// no-op
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
const badCar = await readCarStream(writeCarStream(block0.cid, blockIter()))
|
|
97
|
-
await expect(flush(badCar.blocks)).rejects.toThrow(
|
|
98
|
-
'Not a valid CID for bytes',
|
|
99
|
-
)
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('skips CID verification', async () => {
|
|
103
|
-
const block0 = await dataToCborBlock({ block: 0 })
|
|
104
|
-
const block1 = await dataToCborBlock({ block: 1 })
|
|
105
|
-
const block2 = await dataToCborBlock({ block: 2 })
|
|
106
|
-
const block3 = await dataToCborBlock({ block: 3 })
|
|
107
|
-
const badBlock = await dataToCborBlock({ block: 'bad' })
|
|
108
|
-
const blockIter = async function* () {
|
|
109
|
-
yield block0
|
|
110
|
-
yield block1
|
|
111
|
-
yield block2
|
|
112
|
-
yield { cid: block3.cid, bytes: badBlock.bytes }
|
|
113
|
-
}
|
|
114
|
-
const flush = async function (iter: AsyncIterable<unknown>) {
|
|
115
|
-
for await (const _ of iter) {
|
|
116
|
-
// no-op
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
const badCar = await readCarStream(
|
|
120
|
-
writeCarStream(block0.cid, blockIter()),
|
|
121
|
-
{ skipCidVerification: true },
|
|
122
|
-
)
|
|
123
|
-
await expect(flush(badCar.blocks)).resolves.toBeUndefined()
|
|
124
|
-
})
|
|
125
|
-
})
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { Secp256k1Keypair } from '@atproto/crypto'
|
|
2
|
-
import {
|
|
3
|
-
Repo,
|
|
4
|
-
WriteOpAction,
|
|
5
|
-
blocksToCarFile,
|
|
6
|
-
verifyProofs,
|
|
7
|
-
} from '../src/index.js'
|
|
8
|
-
import { MemoryBlockstore } from '../src/storage/index.js'
|
|
9
|
-
|
|
10
|
-
describe('Commit data', () => {
|
|
11
|
-
// @NOTE this test uses a fully deterministic tree structure
|
|
12
|
-
it('includes all relevant blocks for proof in commit data', async () => {
|
|
13
|
-
const did = 'did:example:alice'
|
|
14
|
-
const collection = 'com.atproto.test'
|
|
15
|
-
const record = {
|
|
16
|
-
test: 123,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const blockstore = new MemoryBlockstore()
|
|
20
|
-
const keypair = await Secp256k1Keypair.create()
|
|
21
|
-
let repo = await Repo.create(blockstore, did, keypair)
|
|
22
|
-
|
|
23
|
-
const keys: string[] = []
|
|
24
|
-
for (let i = 0; i < 50; i++) {
|
|
25
|
-
const rkey = `key-${i}`
|
|
26
|
-
keys.push(rkey)
|
|
27
|
-
repo = await repo.applyWrites(
|
|
28
|
-
[
|
|
29
|
-
{
|
|
30
|
-
action: WriteOpAction.Create,
|
|
31
|
-
collection,
|
|
32
|
-
rkey,
|
|
33
|
-
record,
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
keypair,
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// this test demonstrates the test case:
|
|
41
|
-
// specifically in the case of deleting the first key, there is a "rearranged block" that is necessary
|
|
42
|
-
// in the proof path but _is not_ in newBlocks (as it already existed in the repository)
|
|
43
|
-
{
|
|
44
|
-
const commit = await repo.formatCommit(
|
|
45
|
-
{
|
|
46
|
-
action: WriteOpAction.Delete,
|
|
47
|
-
collection,
|
|
48
|
-
rkey: keys[0],
|
|
49
|
-
},
|
|
50
|
-
keypair,
|
|
51
|
-
)
|
|
52
|
-
const car = await blocksToCarFile(commit.cid, commit.newBlocks)
|
|
53
|
-
const proofAttempt = verifyProofs(
|
|
54
|
-
car,
|
|
55
|
-
[
|
|
56
|
-
{
|
|
57
|
-
collection,
|
|
58
|
-
rkey: keys[0],
|
|
59
|
-
cid: null,
|
|
60
|
-
},
|
|
61
|
-
],
|
|
62
|
-
did,
|
|
63
|
-
keypair.did(),
|
|
64
|
-
)
|
|
65
|
-
await expect(proofAttempt).rejects.toThrow(/block not found/)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
for (const rkey of keys) {
|
|
69
|
-
const commit = await repo.formatCommit(
|
|
70
|
-
{
|
|
71
|
-
action: WriteOpAction.Delete,
|
|
72
|
-
collection,
|
|
73
|
-
rkey,
|
|
74
|
-
},
|
|
75
|
-
keypair,
|
|
76
|
-
)
|
|
77
|
-
const car = await blocksToCarFile(commit.cid, commit.relevantBlocks)
|
|
78
|
-
const proofRes = await verifyProofs(
|
|
79
|
-
car,
|
|
80
|
-
[
|
|
81
|
-
{
|
|
82
|
-
collection,
|
|
83
|
-
rkey: rkey,
|
|
84
|
-
cid: null,
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
did,
|
|
88
|
-
keypair.did(),
|
|
89
|
-
)
|
|
90
|
-
expect(proofRes.unverified.length).toBe(0)
|
|
91
|
-
repo = await repo.applyCommit(commit)
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
-
})
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"comment": "two deep split",
|
|
4
|
-
"leafValue": "bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454",
|
|
5
|
-
"keys": [
|
|
6
|
-
"A0/374913",
|
|
7
|
-
"B1/986427",
|
|
8
|
-
"C0/451630",
|
|
9
|
-
"E0/670489",
|
|
10
|
-
"F1/085263",
|
|
11
|
-
"G0/765327"
|
|
12
|
-
],
|
|
13
|
-
"adds": ["D2/269196"],
|
|
14
|
-
"dels": [],
|
|
15
|
-
"rootBeforeCommit": "bafyreicraprx2xwnico4tuqir3ozsxpz46qkcpox3obf5bagicqwurghpy",
|
|
16
|
-
"rootAfterCommit": "bafyreihvay6pazw3dfa47u5d2tn3rd6pa57sr37bo5bqyvjuqc73ib65my",
|
|
17
|
-
"blocksInProof": [
|
|
18
|
-
"bafyreieazvzmba35p4phksumwfoklwe5o4ncmo7otud74idcyv4orrbzxi",
|
|
19
|
-
"bafyreie4227qpa4vbtbpnsvuhp322b776vjuhxsidi5hxp2gawumr4m3de",
|
|
20
|
-
"bafyreid44jgimksqqdratyste2moqu6zo4h6co2pknjppfoiplsqxtuxae",
|
|
21
|
-
"bafyreiaerlvitye7fjjwodkshtbqqdsmfsdjtnlz4vs6y4trnddshsmd5a",
|
|
22
|
-
"bafyreihvay6pazw3dfa47u5d2tn3rd6pa57sr37bo5bqyvjuqc73ib65my"
|
|
23
|
-
]
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"comment": "two deep leafless split",
|
|
27
|
-
"leafValue": "bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454",
|
|
28
|
-
"keys": ["A0/374913", "B0/601692", "D0/952776", "E0/670489"],
|
|
29
|
-
"adds": ["C2/014073"],
|
|
30
|
-
"dels": [],
|
|
31
|
-
"rootBeforeCommit": "bafyreialm5sgf7pijawbschsjpdevid5rss5ip3d4n4w6cc4mhu53sfl4i",
|
|
32
|
-
"rootAfterCommit": "bafyreibxh4iztp5l2yshz3ectg2qjpeyprpw2gogao3pvceowpq3k3thya",
|
|
33
|
-
"blocksInProof": [
|
|
34
|
-
"bafyreih7dxytqtcjv3cfia3fi3wxofeip62teqkpynnkxisxqwfchfb4bu",
|
|
35
|
-
"bafyreiaqbymlnvpklmogx75gozjl3y73gva43jbgwcrqu2pp5g5ejou5vm",
|
|
36
|
-
"bafyreicfh3st5ghtnoqyyvznjv4lhfnvl7qsndempx35i4tcmoxakqbgrm",
|
|
37
|
-
"bafyreieyjrrai6igjceyxzkajrxgxz37da2eufb33anvesb4ev6yzztauu",
|
|
38
|
-
"bafyreibxh4iztp5l2yshz3ectg2qjpeyprpw2gogao3pvceowpq3k3thya"
|
|
39
|
-
]
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
"comment": "add on edge with neighbor two layers down",
|
|
43
|
-
"leafValue": "bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454",
|
|
44
|
-
"keys": ["A0/374913", "B2/827649", "C0/451630"],
|
|
45
|
-
"adds": ["D2/269196"],
|
|
46
|
-
"dels": [],
|
|
47
|
-
"rootBeforeCommit": "bafyreigc6ay2qwfk7kuevvrczummpd64nknfo4yxpaooknfymzyb7u3ntq",
|
|
48
|
-
"rootAfterCommit": "bafyreign6kxoll35r5f2ske6hjx7vg56aw3jn6r5hcopgrepzafpvohr2a",
|
|
49
|
-
"blocksInProof": [
|
|
50
|
-
"bafyreieazvzmba35p4phksumwfoklwe5o4ncmo7otud74idcyv4orrbzxi",
|
|
51
|
-
"bafyreidicvcjgrpm5bmhm3ndh2ysqfhgzk4chwn3m4kuvwkenfusspb4uy",
|
|
52
|
-
"bafyreign6kxoll35r5f2ske6hjx7vg56aw3jn6r5hcopgrepzafpvohr2a"
|
|
53
|
-
]
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
"comment": "merge and split in multi-op commit",
|
|
57
|
-
"leafValue": "bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454",
|
|
58
|
-
"keys": ["A0/374913", "B2/827649", "D2/269196", "E0/670489"],
|
|
59
|
-
"adds": ["C2/014073"],
|
|
60
|
-
"dels": ["B2/827649", "D2/269196"],
|
|
61
|
-
"rootBeforeCommit": "bafyreiceld4icym4qjmdcn3dfgtxt7t66hdgyhvigessgmkvb56dx6amgi",
|
|
62
|
-
"rootAfterCommit": "bafyreigkalika3taqauapfha556lo36zzcjoiifny5xeru6yis3nxw5ruq",
|
|
63
|
-
"blocksInProof": [
|
|
64
|
-
"bafyreid44jgimksqqdratyste2moqu6zo4h6co2pknjppfoiplsqxtuxae",
|
|
65
|
-
"bafyreihytu6onh476trave25zuo63ziebkeong2755sc5nmf55uzdawgt4",
|
|
66
|
-
"bafyreigkalika3taqauapfha556lo36zzcjoiifny5xeru6yis3nxw5ruq",
|
|
67
|
-
"bafyreidnnkrdkcaswbflgtdsxm7nzs7p5f2rdous6wrlupzstuwqu5pfgm",
|
|
68
|
-
"bafyreia2kq243hqq3volwlzkbzzphoeqauk54sc5h7vgogq4ei5fjizxvy"
|
|
69
|
-
]
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
"comment": "complex multi-op commit",
|
|
73
|
-
"leafValue": "bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454",
|
|
74
|
-
"keys": [
|
|
75
|
-
"B0/601692",
|
|
76
|
-
"C2/014073",
|
|
77
|
-
"D0/952776",
|
|
78
|
-
"E2/819540",
|
|
79
|
-
"F0/697858",
|
|
80
|
-
"H0/131238"
|
|
81
|
-
],
|
|
82
|
-
"adds": ["A2/827942", "G2/611528"],
|
|
83
|
-
"dels": ["C2/014073"],
|
|
84
|
-
"rootBeforeCommit": "bafyreigr3plnts7dax6yokvinbhcqpyicdfgg6npvvyx6okc5jo55slfqi",
|
|
85
|
-
"rootAfterCommit": "bafyreiftrcrbhrwmi37u4egedlg56gk3jeh3tvmqvwgowoifuklfysyx54",
|
|
86
|
-
"blocksInProof": [
|
|
87
|
-
"bafyreih62n3gjbzzvlicuggpfydyzrp3ssyx7hdgtltd3sct3ribm3u73e",
|
|
88
|
-
"bafyreihrjhuoynjvgteuefin5vwnqmupyfzvmytdobpstqt3mbawgw5qhm",
|
|
89
|
-
"bafyreibevzst4gzkxo263syohlmq3lpxdvpjhlpyqx2ay3moh43lifydca",
|
|
90
|
-
"bafyreifsdd7dv2neal7zjhyrsvndkaocelqlpgfxwo4utoq2g77klih37e",
|
|
91
|
-
"bafyreid2wwyroodj2lxx2obikac74q77lsn6vqkoetlqqwwnr3criwlcvy",
|
|
92
|
-
"bafyreie55b224oljhykpsxdjq4ajn2ysksud7qm347s6kn2ei6a775faum",
|
|
93
|
-
"bafyreiftrcrbhrwmi37u4egedlg56gk3jeh3tvmqvwgowoifuklfysyx54"
|
|
94
|
-
]
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
"comment": "split with earlier leaves on same layer",
|
|
98
|
-
"leafValue": "bafyreie5cvv4h45feadgeuwhbcutmh6t2ceseocckahdoe6uat64zmz454",
|
|
99
|
-
"keys": [
|
|
100
|
-
"app.bsky.feed.post/3lo3kqqljmfe2",
|
|
101
|
-
"app.bsky.feed.post/3log4547dm6h2",
|
|
102
|
-
"app.bsky.feed.post/3log45inogon2",
|
|
103
|
-
"app.bsky.feed.post/3logaodrh74d2",
|
|
104
|
-
"app.bsky.feed.post/3logteazog2n2",
|
|
105
|
-
"app.bsky.feed.post/3lon5cqsbwrj2",
|
|
106
|
-
"app.bsky.feed.repost/3l6sjhvqonco2"
|
|
107
|
-
],
|
|
108
|
-
"adds": ["app.bsky.feed.post/3lon5dzeaihj2"],
|
|
109
|
-
"dels": [],
|
|
110
|
-
"rootBeforeCommit": "bafyreigfcsro2up7qi7l3rxdpg7n6gjtteotkmgrrqztl5oy2tf4ncl4ji",
|
|
111
|
-
"rootAfterCommit": "bafyreig33hsjiplaixvmccy65n7rn3in5nsbtcittzx6k3w5wjfhk2sg3a",
|
|
112
|
-
"blocksInProof": [
|
|
113
|
-
"bafyreig33hsjiplaixvmccy65n7rn3in5nsbtcittzx6k3w5wjfhk2sg3a",
|
|
114
|
-
"bafyreih2rhjm3apcghihwfojv2em7noqkgt5qyjcnxux7do674m464oc3m",
|
|
115
|
-
"bafyreiajhswkduap4zvqvfhth3skdgckmk2eb5gow7vv3gvj45f4fqwmxm"
|
|
116
|
-
]
|
|
117
|
-
}
|
|
118
|
-
]
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { parseCid } from '@atproto/lex-data'
|
|
2
|
-
import { BlockMap } from '../src/index.js'
|
|
3
|
-
import { MST } from '../src/mst/index.js'
|
|
4
|
-
import { MemoryBlockstore } from '../src/storage/index.js'
|
|
5
|
-
import fixtures from './commit-proof-fixtures.json' with { type: 'json' }
|
|
6
|
-
|
|
7
|
-
describe('commit proofs', () => {
|
|
8
|
-
for (const fixture of fixtures) {
|
|
9
|
-
it(fixture.comment, async () => {
|
|
10
|
-
const { leafValue, keys, adds, dels } = fixture
|
|
11
|
-
const leaf = parseCid(leafValue)
|
|
12
|
-
|
|
13
|
-
const storage = new MemoryBlockstore()
|
|
14
|
-
let mst = await MST.create(storage)
|
|
15
|
-
for (const key of keys) {
|
|
16
|
-
mst = await mst.add(key, leaf)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const rootBeforeCommit = await mst.getPointer()
|
|
20
|
-
expect(rootBeforeCommit.toString()).toEqual(fixture.rootBeforeCommit)
|
|
21
|
-
|
|
22
|
-
for (const key of adds) {
|
|
23
|
-
mst = await mst.add(key, leaf)
|
|
24
|
-
}
|
|
25
|
-
for (const key of dels) {
|
|
26
|
-
mst = await mst.delete(key)
|
|
27
|
-
}
|
|
28
|
-
const rootAfterCommit = await mst.getPointer()
|
|
29
|
-
expect(rootAfterCommit.toString()).toEqual(fixture.rootAfterCommit)
|
|
30
|
-
const proofs = await Promise.all(
|
|
31
|
-
[...adds, ...dels].map((key) => mst.getCoveringProof(key)),
|
|
32
|
-
)
|
|
33
|
-
const proof = proofs.reduce((acc, cur) => acc.addMap(cur), new BlockMap())
|
|
34
|
-
const blocksInProof = fixture.blocksInProof.map(parseCid)
|
|
35
|
-
for (const cid of blocksInProof) {
|
|
36
|
-
expect(proof.has(cid)).toBe(true)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const invertAdds = adds.map((k) => (mst: MST) => mst.delete(k))
|
|
40
|
-
const invertDels = dels.map((k) => (mst: MST) => mst.add(k, leaf))
|
|
41
|
-
const invertOrders = permutations([...invertAdds, ...invertDels])
|
|
42
|
-
|
|
43
|
-
const proofStorage = new MemoryBlockstore(proof)
|
|
44
|
-
for (const order of invertOrders) {
|
|
45
|
-
let proofMst = await MST.load(proofStorage, rootAfterCommit)
|
|
46
|
-
for (const fn of order) {
|
|
47
|
-
proofMst = await fn(proofMst)
|
|
48
|
-
}
|
|
49
|
-
const rootAfterInvert = await proofMst.getPointer()
|
|
50
|
-
expect(rootAfterInvert.toString()).toEqual(fixture.rootBeforeCommit)
|
|
51
|
-
}
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
function permutations<T>(arr: T[]): T[][] {
|
|
57
|
-
if (arr.length <= 1) return [arr]
|
|
58
|
-
|
|
59
|
-
return arr.reduce((perms: T[][], item: T, i: number) => {
|
|
60
|
-
const rest = [...arr.slice(0, i), ...arr.slice(i + 1)]
|
|
61
|
-
return perms.concat(permutations(rest).map((p) => [item, ...p]))
|
|
62
|
-
}, [])
|
|
63
|
-
}
|