A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: dasl sync

+446 -1
+1
src/_includes/layouts/diffuse.vto
··· 51 51 "@awesome.me/webawesome/dist-cdn/": "./vendor/@awesome.me/webawesome/", 52 52 "@phosphor-icons/web/": "./vendor/@phosphor-icons/web/", 53 53 54 + "@atcute/cbor": "./vendor/@atcute/cbor/index.js", 54 55 "@atcute/tid": "./vendor/@atcute/tid/index.js", 55 56 "idb-keyval": "./vendor/idb-keyval/index.js", 56 57 "lit-html": "./vendor/lit-html/index.js",
+1 -1
src/components/transformer/output/bytes/dasl-sync/element.js
··· 345 345 /** @type {T} */ 346 346 const mergedItem = { ...oldItem }; 347 347 348 - deepDiff.applyDiff(newestItem, mergedItem); 348 + deepDiff.applyDiff(mergedItem, newestItem); 349 349 350 350 data.push(mergedItem); 351 351
+1
src/vendor/@atcute/cbor/index.js
··· 1 + export * from "@atcute/cbor";
+443
tests/components/transformer/output/bytes/dasl-sync/test.ts
··· 1 + import { describe, it } from "@std/testing/bdd"; 2 + import { expect } from "@std/expect"; 3 + 4 + import { testWeb } from "@tests/common/index.ts"; 5 + 6 + describe("components/transformer/output/bytes/dasl-sync", () => { 7 + describe("updateContainer", () => { 8 + it("returns an empty container for an empty collection", async () => { 9 + const result = await testWeb(async () => { 10 + const { CLASS } = await import( 11 + "~/components/transformer/output/bytes/dasl-sync/element.js" 12 + ); 13 + const el = new CLASS(); 14 + const EMPTY = { 15 + cid: undefined, 16 + data: [], 17 + inventory: { current: {}, removed: [] }, 18 + }; 19 + return await el.updateContainer({ previous: EMPTY, collection: [] }); 20 + }); 21 + 22 + expect(result.data).toEqual([]); 23 + expect(result.inventory.current).toEqual({}); 24 + expect(result.inventory.removed).toEqual([]); 25 + }); 26 + 27 + it("assigns CIDs to newly added items", async () => { 28 + const result = await testWeb(async () => { 29 + const { CLASS } = await import( 30 + "~/components/transformer/output/bytes/dasl-sync/element.js" 31 + ); 32 + const el = new CLASS(); 33 + const EMPTY = { 34 + cid: undefined, 35 + data: [], 36 + inventory: { current: {}, removed: [] }, 37 + }; 38 + const items = [ 39 + { id: "a", updatedAt: "2024-01-01T00:00:00Z" }, 40 + { id: "b", updatedAt: "2024-01-02T00:00:00Z" }, 41 + ]; 42 + const c = await el.updateContainer({ previous: EMPTY, collection: items }); 43 + return { 44 + dataLength: c.data.length, 45 + currentKeys: Object.keys(c.inventory.current), 46 + removed: c.inventory.removed, 47 + cidType: typeof c.cid, 48 + cidA: typeof c.inventory.current["a"], 49 + cidB: typeof c.inventory.current["b"], 50 + }; 51 + }); 52 + 53 + expect(result.dataLength).toBe(2); 54 + expect(result.currentKeys.sort()).toEqual(["a", "b"]); 55 + expect(result.removed).toEqual([]); 56 + expect(result.cidType).toBe("string"); 57 + expect(result.cidA).toBe("string"); 58 + expect(result.cidB).toBe("string"); 59 + }); 60 + 61 + it("moves removed items from current to removed list", async () => { 62 + const result = await testWeb(async () => { 63 + const { CLASS } = await import( 64 + "~/components/transformer/output/bytes/dasl-sync/element.js" 65 + ); 66 + const el = new CLASS(); 67 + const EMPTY = { 68 + cid: undefined, 69 + data: [], 70 + inventory: { current: {}, removed: [] }, 71 + }; 72 + const items = [ 73 + { id: "a", updatedAt: "2024-01-01T00:00:00Z" }, 74 + { id: "b", updatedAt: "2024-01-01T00:00:00Z" }, 75 + ]; 76 + const first = await el.updateContainer({ 77 + previous: EMPTY, 78 + collection: items, 79 + }); 80 + const second = await el.updateContainer({ 81 + previous: first, 82 + collection: [items[1]], 83 + }); 84 + return { 85 + dataLength: second.data.length, 86 + dataId: second.data[0]?.id, 87 + hasA: "a" in second.inventory.current, 88 + removed: second.inventory.removed, 89 + }; 90 + }); 91 + 92 + expect(result.dataLength).toBe(1); 93 + expect(result.dataId).toBe("b"); 94 + expect(result.hasA).toBe(false); 95 + expect(result.removed).toContain("a"); 96 + }); 97 + 98 + it("preserves existing CIDs for unchanged items", async () => { 99 + const result = await testWeb(async () => { 100 + const { CLASS } = await import( 101 + "~/components/transformer/output/bytes/dasl-sync/element.js" 102 + ); 103 + const el = new CLASS(); 104 + const EMPTY = { 105 + cid: undefined, 106 + data: [], 107 + inventory: { current: {}, removed: [] }, 108 + }; 109 + const items = [{ id: "a", updatedAt: "2024-01-01T00:00:00Z" }]; 110 + const first = await el.updateContainer({ 111 + previous: EMPTY, 112 + collection: items, 113 + }); 114 + const second = await el.updateContainer({ 115 + previous: first, 116 + collection: items, 117 + }); 118 + return { 119 + cidFirst: first.inventory.current["a"], 120 + cidSecond: second.inventory.current["a"], 121 + }; 122 + }); 123 + 124 + expect(result.cidFirst).toBe(result.cidSecond); 125 + }); 126 + 127 + it("accumulates removed items across multiple updates", async () => { 128 + const result = await testWeb(async () => { 129 + const { CLASS } = await import( 130 + "~/components/transformer/output/bytes/dasl-sync/element.js" 131 + ); 132 + const el = new CLASS(); 133 + const EMPTY = { 134 + cid: undefined, 135 + data: [], 136 + inventory: { current: {}, removed: [] }, 137 + }; 138 + const items = [ 139 + { id: "a", updatedAt: "2024-01-01T00:00:00Z" }, 140 + { id: "b", updatedAt: "2024-01-01T00:00:00Z" }, 141 + { id: "c", updatedAt: "2024-01-01T00:00:00Z" }, 142 + ]; 143 + const first = await el.updateContainer({ 144 + previous: EMPTY, 145 + collection: items, 146 + }); 147 + const second = await el.updateContainer({ 148 + previous: first, 149 + collection: [items[1], items[2]], 150 + }); 151 + const third = await el.updateContainer({ 152 + previous: second, 153 + collection: [items[2]], 154 + }); 155 + return third.inventory.removed; 156 + }); 157 + 158 + expect(result).toContain("a"); 159 + expect(result).toContain("b"); 160 + }); 161 + }); 162 + 163 + describe("hasDiverged", () => { 164 + it("returns false when CIDs are equal", async () => { 165 + const result = await testWeb(async () => { 166 + const { CLASS } = await import( 167 + "~/components/transformer/output/bytes/dasl-sync/element.js" 168 + ); 169 + const el = new CLASS(); 170 + return el.hasDiverged({ 171 + local: { cid: "bafyabc", data: [], inventory: { current: {}, removed: [] } }, 172 + remote: { cid: "bafyabc", data: [], inventory: { current: {}, removed: [] } }, 173 + }); 174 + }); 175 + 176 + expect(result).toBe(false); 177 + }); 178 + 179 + it("returns true when CIDs differ", async () => { 180 + const result = await testWeb(async () => { 181 + const { CLASS } = await import( 182 + "~/components/transformer/output/bytes/dasl-sync/element.js" 183 + ); 184 + const el = new CLASS(); 185 + return el.hasDiverged({ 186 + local: { cid: "bafyabc", data: [], inventory: { current: {}, removed: [] } }, 187 + remote: { cid: "bafyxyz", data: [], inventory: { current: {}, removed: [] } }, 188 + }); 189 + }); 190 + 191 + expect(result).toBe(true); 192 + }); 193 + 194 + it("returns false when both CIDs are undefined", async () => { 195 + const result = await testWeb(async () => { 196 + const { CLASS } = await import( 197 + "~/components/transformer/output/bytes/dasl-sync/element.js" 198 + ); 199 + const el = new CLASS(); 200 + return el.hasDiverged({ 201 + local: { cid: undefined, data: [], inventory: { current: {}, removed: [] } }, 202 + remote: { cid: undefined, data: [], inventory: { current: {}, removed: [] } }, 203 + }); 204 + }); 205 + 206 + expect(result).toBe(false); 207 + }); 208 + 209 + it("returns true when one CID is undefined", async () => { 210 + const result = await testWeb(async () => { 211 + const { CLASS } = await import( 212 + "~/components/transformer/output/bytes/dasl-sync/element.js" 213 + ); 214 + const el = new CLASS(); 215 + return el.hasDiverged({ 216 + local: { cid: "bafyabc", data: [], inventory: { current: {}, removed: [] } }, 217 + remote: { cid: undefined, data: [], inventory: { current: {}, removed: [] } }, 218 + }); 219 + }); 220 + 221 + expect(result).toBe(true); 222 + }); 223 + }); 224 + 225 + describe("merge", () => { 226 + it("includes non-overlapping items from both containers", async () => { 227 + const result = await testWeb(async () => { 228 + const { CLASS } = await import( 229 + "~/components/transformer/output/bytes/dasl-sync/element.js" 230 + ); 231 + const el = new CLASS(); 232 + const containerA = { 233 + cid: "cid-a", 234 + data: [{ id: "a", updatedAt: "2024-01-01T00:00:00Z" }], 235 + inventory: { current: { a: "cid-item-a" }, removed: [] }, 236 + }; 237 + const containerB = { 238 + cid: "cid-b", 239 + data: [{ id: "b", updatedAt: "2024-01-01T00:00:00Z" }], 240 + inventory: { current: { b: "cid-item-b" }, removed: [] }, 241 + }; 242 + const merged = await el.merge(containerA, containerB); 243 + return { ids: merged.data.map((i: { id: string }) => i.id).sort(), removed: merged.inventory.removed }; 244 + }); 245 + 246 + expect(result.ids).toEqual(["a", "b"]); 247 + expect(result.removed).toEqual([]); 248 + }); 249 + 250 + it("respects items removed on side A", async () => { 251 + const result = await testWeb(async () => { 252 + const { CLASS } = await import( 253 + "~/components/transformer/output/bytes/dasl-sync/element.js" 254 + ); 255 + const el = new CLASS(); 256 + const itemA = { id: "a", updatedAt: "2024-01-01T00:00:00Z" }; 257 + const itemB = { id: "b", updatedAt: "2024-01-01T00:00:00Z" }; 258 + // A has removed "b" 259 + const containerA = { 260 + cid: "cid-a", 261 + data: [itemA], 262 + inventory: { current: { a: "cid-item-a" }, removed: ["b"] }, 263 + }; 264 + // B still has "b" 265 + const containerB = { 266 + cid: "cid-b", 267 + data: [itemA, itemB], 268 + inventory: { current: { a: "cid-item-a", b: "cid-item-b" }, removed: [] }, 269 + }; 270 + const merged = await el.merge(containerA, containerB); 271 + return { ids: merged.data.map((i: { id: string }) => i.id), removed: merged.inventory.removed }; 272 + }); 273 + 274 + expect(result.ids).not.toContain("b"); 275 + expect(result.removed).toContain("b"); 276 + }); 277 + 278 + it("respects items removed on side B", async () => { 279 + const result = await testWeb(async () => { 280 + const { CLASS } = await import( 281 + "~/components/transformer/output/bytes/dasl-sync/element.js" 282 + ); 283 + const el = new CLASS(); 284 + const itemA = { id: "a", updatedAt: "2024-01-01T00:00:00Z" }; 285 + const itemB = { id: "b", updatedAt: "2024-01-01T00:00:00Z" }; 286 + // A still has "b" 287 + const containerA = { 288 + cid: "cid-a", 289 + data: [itemA, itemB], 290 + inventory: { current: { a: "cid-item-a", b: "cid-item-b" }, removed: [] }, 291 + }; 292 + // B has removed "b" 293 + const containerB = { 294 + cid: "cid-b", 295 + data: [itemA], 296 + inventory: { current: { a: "cid-item-a" }, removed: ["b"] }, 297 + }; 298 + const merged = await el.merge(containerA, containerB); 299 + return { ids: merged.data.map((i: { id: string }) => i.id), removed: merged.inventory.removed }; 300 + }); 301 + 302 + expect(result.ids).not.toContain("b"); 303 + expect(result.removed).toContain("b"); 304 + }); 305 + 306 + it("uses the newer item when the same item conflicts", async () => { 307 + const result = await testWeb(async () => { 308 + const { CLASS } = await import( 309 + "~/components/transformer/output/bytes/dasl-sync/element.js" 310 + ); 311 + const el = new CLASS(); 312 + const oldItem = { id: "a", updatedAt: "2024-01-01T00:00:00Z", name: "Old" }; 313 + const newItem = { id: "a", updatedAt: "2024-06-01T00:00:00Z", name: "New" }; 314 + const containerA = { 315 + cid: "cid-a", 316 + data: [oldItem], 317 + inventory: { current: { a: "cid-old" }, removed: [] }, 318 + }; 319 + const containerB = { 320 + cid: "cid-b", 321 + data: [newItem], 322 + inventory: { current: { a: "cid-new" }, removed: [] }, 323 + }; 324 + const merged = await el.merge(containerA, containerB); 325 + return (merged.data[0] as { name: string }).name; 326 + }); 327 + 328 + // The newer item ("New") should win the conflict 329 + expect(result).toBe("New"); 330 + }); 331 + 332 + it("keeps identical items without recomputing their CIDs", async () => { 333 + const result = await testWeb(async () => { 334 + const { CLASS } = await import( 335 + "~/components/transformer/output/bytes/dasl-sync/element.js" 336 + ); 337 + const el = new CLASS(); 338 + const item = { id: "a", updatedAt: "2024-01-01T00:00:00Z" }; 339 + const container = { 340 + cid: "cid-a", 341 + data: [item], 342 + inventory: { current: { a: "original-cid" }, removed: [] }, 343 + }; 344 + const merged = await el.merge(container, container); 345 + return merged.inventory.current["a"]; 346 + }); 347 + 348 + expect(result).toBe("original-cid"); 349 + }); 350 + 351 + it("combines removed lists from both containers", async () => { 352 + const result = await testWeb(async () => { 353 + const { CLASS } = await import( 354 + "~/components/transformer/output/bytes/dasl-sync/element.js" 355 + ); 356 + const el = new CLASS(); 357 + const containerA = { 358 + cid: "cid-a", 359 + data: [], 360 + inventory: { current: {}, removed: ["x"] }, 361 + }; 362 + const containerB = { 363 + cid: "cid-b", 364 + data: [], 365 + inventory: { current: {}, removed: ["y"] }, 366 + }; 367 + const merged = await el.merge(containerA, containerB); 368 + return merged.inventory.removed; 369 + }); 370 + 371 + expect(result).toContain("x"); 372 + expect(result).toContain("y"); 373 + }); 374 + 375 + it("produces a deterministic CID from the merged inventory", async () => { 376 + const result = await testWeb(async () => { 377 + const { CLASS } = await import( 378 + "~/components/transformer/output/bytes/dasl-sync/element.js" 379 + ); 380 + const el = new CLASS(); 381 + const containerA = { 382 + cid: "cid-a", 383 + data: [{ id: "a", updatedAt: "2024-01-01T00:00:00Z" }], 384 + inventory: { current: { a: "cid-item-a" }, removed: [] }, 385 + }; 386 + const containerB = { 387 + cid: "cid-b", 388 + data: [{ id: "b", updatedAt: "2024-01-01T00:00:00Z" }], 389 + inventory: { current: { b: "cid-item-b" }, removed: [] }, 390 + }; 391 + const merge1 = await el.merge(containerA, containerB); 392 + const merge2 = await el.merge(containerA, containerB); 393 + return { cid1: merge1.cid, cid2: merge2.cid }; 394 + }); 395 + 396 + expect(result.cid1).toBe(result.cid2); 397 + expect(typeof result.cid1).toBe("string"); 398 + }); 399 + }); 400 + 401 + describe("save", () => { 402 + it("round-trips a container through CBOR encode/decode", async () => { 403 + const result = await testWeb(async () => { 404 + const { decode } = await import("@atcute/cbor"); 405 + const { CLASS } = await import( 406 + "~/components/transformer/output/bytes/dasl-sync/element.js" 407 + ); 408 + const el = new CLASS(); 409 + const container = { 410 + cid: "bafyabc", 411 + data: [{ id: "a", updatedAt: "2024-01-01T00:00:00Z", value: 42 }], 412 + inventory: { current: { a: "cid-a" }, removed: [] }, 413 + }; 414 + const bytes = el.save(container); 415 + return decode(bytes); 416 + }); 417 + 418 + expect(result.cid).toBe("bafyabc"); 419 + expect(result.data.length).toBe(1); 420 + expect(result.data[0].id).toBe("a"); 421 + expect(result.data[0].value).toBe(42); 422 + expect(result.inventory.current).toEqual({ a: "cid-a" }); 423 + }); 424 + 425 + it("returns a Uint8Array", async () => { 426 + const result = await testWeb(async () => { 427 + const { CLASS } = await import( 428 + "~/components/transformer/output/bytes/dasl-sync/element.js" 429 + ); 430 + const el = new CLASS(); 431 + const container = { 432 + cid: undefined, 433 + data: [], 434 + inventory: { current: {}, removed: [] }, 435 + }; 436 + const bytes = el.save(container); 437 + return bytes instanceof Uint8Array; 438 + }); 439 + 440 + expect(result).toBe(true); 441 + }); 442 + }); 443 + });