"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
const node_assert_1 = require("node:assert");
const internal_1 = require("@fluidframework/runtime-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const index_js_1 = require("../../blobManager/index.js");
const blobTestUtils_js_1 = require("./blobTestUtils.js");
for (const createBlobPayloadPending of [false, true]) {
    describe(`BlobManager (pending payloads: ${createBlobPayloadPending})`, () => {
        // #region Detached usage
        describe("Detached usage", () => {
            it("Responds as expected for retrieving unknown blob IDs", async () => {
                const { blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                    attached: false,
                    createBlobPayloadPending,
                });
                (0, node_assert_1.strict)(!blobManager.hasBlob("blobId"));
                await node_assert_1.strict.rejects(async () => {
                    // Handles for detached blobs are never payload pending, even if the flag is set.
                    await blobManager.getBlob("blobId", false);
                });
            });
            it("Can create a blob and retrieve it", async () => {
                const { mockOrderingService, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                    attached: false,
                    createBlobPayloadPending,
                });
                const handle = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                const { localId } = (0, blobTestUtils_js_1.unpackHandle)(handle);
                (0, node_assert_1.strict)(blobManager.hasBlob(localId));
                // Handles for detached blobs are never payload pending, even if the flag is set.
                (0, node_assert_1.strict)(!handle.payloadPending);
                const blobFromManager = await blobManager.getBlob(localId, false);
                node_assert_1.strict.strictEqual((0, blobTestUtils_js_1.blobToText)(blobFromManager), "hello", "Blob content mismatch");
                const blobFromHandle = await handle.get();
                node_assert_1.strict.strictEqual((0, blobTestUtils_js_1.blobToText)(blobFromHandle), "hello", "Blob content mismatch");
                node_assert_1.strict.strictEqual(mockOrderingService.messagesReceived, 0, "Should not try to send messages in detached state");
            });
        });
        describe("Attaching", () => {
            it("Can attach when empty", async () => {
                const { mockBlobStorage, mockRuntime, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                    attached: false,
                    createBlobPayloadPending,
                });
                await (0, blobTestUtils_js_1.simulateAttach)(mockBlobStorage, mockRuntime, blobManager);
            });
            it("Can get a detached blob after attaching", async () => {
                const { mockBlobStorage, mockRuntime, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                    attached: false,
                    createBlobPayloadPending,
                });
                const handle = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                const { localId } = (0, blobTestUtils_js_1.unpackHandle)(handle);
                await (0, blobTestUtils_js_1.simulateAttach)(mockBlobStorage, mockRuntime, blobManager);
                (0, node_assert_1.strict)(blobManager.hasBlob(localId));
                // Handles for detached blobs are never payload pending, even if the flag is set.
                const blobFromManager = await blobManager.getBlob(localId, false);
                node_assert_1.strict.strictEqual((0, blobTestUtils_js_1.blobToText)(blobFromManager), "hello", "Blob content mismatch");
                const blobFromHandle = await handle.get();
                node_assert_1.strict.strictEqual((0, blobTestUtils_js_1.blobToText)(blobFromHandle), "hello", "Blob content mismatch");
            });
        });
        // #region Attached usage
        describe("Attached usage", () => {
            describe("Normal usage", () => {
                it("Responds as expected for unknown blob IDs", async () => {
                    const { mockBlobStorage, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        createBlobPayloadPending,
                    });
                    (0, node_assert_1.strict)(!blobManager.hasBlob("blobId"));
                    // When payloadPending is false, we throw for unknown blobs
                    await node_assert_1.strict.rejects(async () => {
                        await blobManager.getBlob("blobId", false);
                    });
                    // When payloadPending is true, we allow the promise to remain pending (waiting
                    // for the blob to later arrive)
                    const getBlobP = blobManager.getBlob("blobId", true);
                    // Simulate the blob being created by some remote client
                    const { id: remoteId } = await mockBlobStorage.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    blobManager.processBlobAttachMessage(
                    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
                    { metadata: { localId: "blobId", blobId: remoteId } }, false);
                    const blob = await getBlobP;
                    node_assert_1.strict.strictEqual((0, blobTestUtils_js_1.blobToText)(blob), "hello", "Blob content mismatch");
                });
                // TODO: Separate test for the network-visible effects of blob creation?
                it("Can create a blob and retrieve it", async () => {
                    const { blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({ createBlobPayloadPending });
                    const handle = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    const { localId } = (0, blobTestUtils_js_1.unpackHandle)(handle);
                    // Even though in the payloadPending case we haven't actually uploaded the blob yet, we
                    // should still be able to check for it and retrieve it using the locally-cached copy.
                    (0, node_assert_1.strict)(blobManager.hasBlob(localId));
                    const _blobFromManager = await blobManager.getBlob(localId, createBlobPayloadPending);
                    node_assert_1.strict.strictEqual((0, blobTestUtils_js_1.blobToText)(_blobFromManager), "hello", "Blob content mismatch");
                    node_assert_1.strict.strictEqual(handle.payloadPending, createBlobPayloadPending, "Wrong handle type created");
                    const blobFromHandle = await handle.get();
                    node_assert_1.strict.strictEqual((0, blobTestUtils_js_1.blobToText)(blobFromHandle), "hello", "Blob content mismatch");
                    (0, node_assert_1.strict)((0, internal_1.isFluidHandlePayloadPending)(handle));
                    // With payloadPending handles, we won't actually upload and send the attach message until the
                    // handle is attached.
                    if (createBlobPayloadPending) {
                        node_assert_1.strict.strictEqual(handle.payloadState, "pending", "Payload should be pending before handle attach");
                        let eventRaised = false;
                        const onPayloadShared = () => {
                            eventRaised = true;
                            handle.events.off("payloadShared", onPayloadShared);
                        };
                        handle.events.on("payloadShared", onPayloadShared);
                        await (0, blobTestUtils_js_1.ensureBlobsShared)([handle]);
                        (0, node_assert_1.strict)(eventRaised, "payloadShared event was not raised when expected");
                    }
                    node_assert_1.strict.strictEqual(handle.payloadState, "shared", "Payload should be in shared state");
                    (0, node_assert_1.strict)(blobManager.hasBlob(localId));
                    const blobFromManager = await blobManager.getBlob(localId, createBlobPayloadPending);
                    node_assert_1.strict.strictEqual((0, blobTestUtils_js_1.blobToText)(blobFromManager), "hello", "Blob content mismatch");
                });
                it("Can retrieve a blob using its storageId", async () => {
                    const { mockOrderingService, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        createBlobPayloadPending,
                    });
                    let storageId;
                    const onMessageReceived = (message) => {
                        storageId = message.metadata.blobId;
                        mockOrderingService.events.off("messageReceived", onMessageReceived);
                    };
                    mockOrderingService.events.on("messageReceived", onMessageReceived);
                    const handle = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    if (createBlobPayloadPending) {
                        await (0, blobTestUtils_js_1.ensureBlobsShared)([handle]);
                    }
                    (0, node_assert_1.strict)(storageId !== undefined, "Should have been able to sniff out the storageId");
                    (0, node_assert_1.strict)(blobManager.hasBlob(storageId));
                    const blobFromManager = await blobManager.getBlob(storageId, createBlobPayloadPending);
                    node_assert_1.strict.strictEqual((0, blobTestUtils_js_1.blobToText)(blobFromManager), "hello", "Blob content mismatch");
                });
                it("Retrieves locally-created blobs without making a storage call", async () => {
                    const mockBlobStorage = new blobTestUtils_js_1.MockStorageAdapter(true);
                    mockBlobStorage.readBlob = async () => {
                        throw new Error("BOOM!");
                    };
                    const { blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        mockBlobStorage,
                        createBlobPayloadPending,
                    });
                    const handle = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    const { localId } = (0, blobTestUtils_js_1.unpackHandle)(handle);
                    if (createBlobPayloadPending) {
                        await (0, blobTestUtils_js_1.ensureBlobsShared)([handle]);
                    }
                    // If this tries to make a storage request it will throw.
                    (0, node_assert_1.strict)(blobManager.hasBlob(localId));
                    const blobFromManager = await blobManager.getBlob(localId, createBlobPayloadPending);
                    node_assert_1.strict.strictEqual((0, blobTestUtils_js_1.blobToText)(blobFromManager), "hello", "Blob content mismatch");
                    const blobFromHandle = await handle.get();
                    node_assert_1.strict.strictEqual((0, blobTestUtils_js_1.blobToText)(blobFromHandle), "hello", "Blob content mismatch");
                });
                it("Does not log an error if runtime is disposed during readBlob error", async () => {
                    const mockBlobStorage = new blobTestUtils_js_1.MockStorageAdapter(true);
                    mockBlobStorage.readBlob = async () => {
                        throw new Error("BOOM!");
                    };
                    const localId = "someLocalId";
                    const remoteId = "someRemoteId";
                    const { mockLogger, mockRuntime, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        mockBlobStorage,
                        blobManagerLoadInfo: {
                            ids: [remoteId],
                            redirectTable: [[localId, remoteId]],
                        },
                        createBlobPayloadPending,
                    });
                    mockRuntime.disposed = true;
                    const getBlobP = blobManager.getBlob(localId, createBlobPayloadPending);
                    await node_assert_1.strict.rejects(getBlobP, { message: "BOOM!" }, "Expected getBlob to throw with test error message");
                    mockLogger.assertMatchNone([{ category: "error" }], "Should not have logged any errors", undefined, false /* clearEventsAfterCheck */);
                    mockLogger.assertMatch([{ category: "generic", eventName: "BlobManager:AttachmentReadBlob_cancel" }], "Expected the _cancel event to be logged with 'generic' category");
                });
                it("Fails as expected for upload failure", async () => {
                    const { mockBlobStorage, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        createBlobPayloadPending,
                    });
                    mockBlobStorage.pause();
                    if (createBlobPayloadPending) {
                        const handle = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                        node_assert_1.strict.strict((0, internal_1.isLocalFluidHandle)(handle));
                        node_assert_1.strict.strictEqual(handle.payloadState, "pending", "Handle should be in pending state");
                        node_assert_1.strict.strictEqual(handle.payloadShareError, undefined, "handle should not have an error yet");
                        let eventRaised = false;
                        const onPayloadShareFailed = (error) => {
                            eventRaised = true;
                            node_assert_1.strict.strictEqual(error.message, "fake driver error", "Did not receive the expected error");
                            handle.events.off("payloadShareFailed", onPayloadShareFailed);
                        };
                        handle.events.on("payloadShareFailed", onPayloadShareFailed);
                        (0, blobTestUtils_js_1.attachHandle)(handle);
                        await mockBlobStorage.waitCreateOne({
                            error: new internal_2.LoggingError("fake driver error"),
                        });
                        mockBlobStorage.unpause();
                        await node_assert_1.strict.rejects((0, blobTestUtils_js_1.waitHandlePayloadShared)(handle), {
                            message: "fake driver error",
                        });
                        node_assert_1.strict.strict(eventRaised, "should emit payloadShareFailed");
                        node_assert_1.strict.strictEqual(handle.payloadState, "pending", "Handle should still be in pending state");
                        node_assert_1.strict.strictEqual(handle.payloadShareError.message, "fake driver error", "Handle did not have the expected error");
                    }
                    else {
                        // If the blobs are created without pending payloads, we don't get to see the handle at
                        // all so we can't inspect its state.
                        const createBlobP = blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                        await mockBlobStorage.waitCreateOne({
                            error: new internal_2.LoggingError("fake driver error"),
                        });
                        mockBlobStorage.unpause();
                        await node_assert_1.strict.rejects(createBlobP, { message: "fake driver error" });
                    }
                    const { ids, redirectTable } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    node_assert_1.strict.strictEqual(ids, undefined);
                    node_assert_1.strict.strictEqual(redirectTable, undefined);
                });
                it("Reuploads blobs if they are expired", async () => {
                    const { mockBlobStorage, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        createBlobPayloadPending,
                    });
                    mockBlobStorage.pause();
                    const handleP = blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    if (createBlobPayloadPending) {
                        const _handle = await handleP;
                        (0, blobTestUtils_js_1.attachHandle)(_handle);
                    }
                    // Use a negative TTL to force the blob to be expired immediately
                    await mockBlobStorage.waitCreateOne({ minTTLOverride: -1 });
                    // After unpausing, the second attempt will be processed with a normal TTL
                    mockBlobStorage.unpause();
                    const handle = await handleP;
                    await (0, blobTestUtils_js_1.ensureBlobsShared)([handle]);
                    node_assert_1.strict.strictEqual(mockBlobStorage.blobsCreated, 2, "Blob should have been reuploaded once");
                    const { ids, redirectTable } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    node_assert_1.strict.strictEqual(ids?.length, 1);
                    node_assert_1.strict.strictEqual(redirectTable?.length, 1);
                });
            });
            describe("Blobs from remote clients", () => {
                it("Can retrieve blobs uploaded by a remote client", async () => {
                    const { mockBlobStorage, mockOrderingService, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        createBlobPayloadPending,
                    });
                    const { blobManager: blobManager2 } = (0, blobTestUtils_js_1.createTestMaterial)({
                        mockBlobStorage,
                        mockOrderingService,
                        createBlobPayloadPending,
                    });
                    const handle = await blobManager2.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    const { localId } = (0, blobTestUtils_js_1.unpackHandle)(handle);
                    if (createBlobPayloadPending) {
                        await (0, blobTestUtils_js_1.ensureBlobsShared)([handle]);
                    }
                    (0, node_assert_1.strict)(blobManager.hasBlob(localId));
                    const blobFromManager = await blobManager.getBlob(localId, createBlobPayloadPending);
                    node_assert_1.strict.strictEqual((0, blobTestUtils_js_1.blobToText)(blobFromManager), "hello", "Blob content mismatch");
                });
                it("Handles deduping against a remote blob", async () => {
                    const { mockBlobStorage, mockOrderingService, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        createBlobPayloadPending,
                    });
                    const { blobManager: blobManager2 } = (0, blobTestUtils_js_1.createTestMaterial)({
                        mockBlobStorage,
                        mockOrderingService,
                        createBlobPayloadPending,
                    });
                    const remoteHandle = await blobManager2.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    if (createBlobPayloadPending) {
                        await (0, blobTestUtils_js_1.ensureBlobsShared)([remoteHandle]);
                    }
                    const localHandle = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    if (createBlobPayloadPending) {
                        await (0, blobTestUtils_js_1.ensureBlobsShared)([localHandle]);
                    }
                    const { ids, redirectTable } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    node_assert_1.strict.strictEqual(ids?.length, 1);
                    node_assert_1.strict.strictEqual(redirectTable?.length, 2);
                });
                it("Handles deduping against a remote blob between upload and BlobAttach", async () => {
                    const { mockBlobStorage, mockOrderingService, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        createBlobPayloadPending,
                    });
                    const { blobManager: blobManager2 } = (0, blobTestUtils_js_1.createTestMaterial)({
                        mockBlobStorage,
                        mockOrderingService,
                        createBlobPayloadPending,
                    });
                    mockOrderingService.pause();
                    const remoteHandleP = blobManager2.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    const localHandleP = blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    if (createBlobPayloadPending) {
                        const [remoteHandle, localHandle] = await Promise.all([
                            remoteHandleP,
                            localHandleP,
                        ]);
                        // Attach the handle to generate an message, but don't wait for the blob to be shared yet
                        // until we unpause the ordering service and can process those messages.
                        (0, blobTestUtils_js_1.attachHandle)(remoteHandle);
                        (0, blobTestUtils_js_1.attachHandle)(localHandle);
                    }
                    // Ensure both blobs have completed upload and are waiting for their messages to be ack'd
                    await new Promise((resolve) => {
                        if (mockOrderingService.messagesReceived !== 2) {
                            const onMessageReceived = () => {
                                if (mockOrderingService.messagesReceived === 2) {
                                    resolve();
                                    mockOrderingService.events.off("messageReceived", onMessageReceived);
                                }
                            };
                            mockOrderingService.events.on("messageReceived", onMessageReceived);
                        }
                    });
                    mockOrderingService.unpause();
                    // Await the handle promises here for the legacy createBlob flow
                    await Promise.all([remoteHandleP, localHandleP]);
                    const { ids, redirectTable } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    node_assert_1.strict.strictEqual(ids?.length, 1);
                    node_assert_1.strict.strictEqual(redirectTable?.length, 2);
                });
            });
            describe("Resubmit", () => {
                it("Can complete blob attach with resubmit", async () => {
                    const { mockOrderingService, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        createBlobPayloadPending,
                    });
                    mockOrderingService.pause();
                    // Generate the original message
                    const handleP = blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    if (createBlobPayloadPending) {
                        const _handle = await handleP;
                        _handle.attachGraph();
                    }
                    // Drop the original message
                    await mockOrderingService.waitDropOne();
                    // Sequence the resubmitted message
                    await mockOrderingService.waitSequenceOne();
                    const handle = await handleP;
                    if (createBlobPayloadPending) {
                        await (0, blobTestUtils_js_1.ensureBlobsShared)([handle]);
                    }
                    (0, node_assert_1.strict)((0, internal_1.isFluidHandlePayloadPending)(handle));
                    node_assert_1.strict.strictEqual(handle.payloadState, "shared", "Payload should be shared");
                    const { ids, redirectTable } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    node_assert_1.strict.strictEqual(ids?.length, 1);
                    node_assert_1.strict.strictEqual(redirectTable?.length, 1);
                });
                it("Can complete blob attach with multiple resubmits", async () => {
                    const { mockOrderingService, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        createBlobPayloadPending,
                    });
                    mockOrderingService.pause();
                    // Generate the original message
                    const handle1P = blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    if (createBlobPayloadPending) {
                        const _handle = await handle1P;
                        _handle.attachGraph();
                    }
                    // Drop the original message for handle1
                    await mockOrderingService.waitDropOne();
                    const handle2P = blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("world"));
                    if (createBlobPayloadPending) {
                        const _handle = await handle2P;
                        _handle.attachGraph();
                    }
                    // Drop the resubmitted message for handle1
                    await mockOrderingService.waitDropOne();
                    // Drop the original message for handle2
                    await mockOrderingService.waitDropOne();
                    // Sequence the doubly-resubmitted message for handle1
                    await mockOrderingService.waitSequenceOne();
                    // Sequence the resubmitted message for handle2
                    await mockOrderingService.waitSequenceOne();
                    const handle1 = await handle1P;
                    const handle2 = await handle2P;
                    if (createBlobPayloadPending) {
                        await (0, blobTestUtils_js_1.ensureBlobsShared)([handle1, handle2]);
                    }
                    (0, node_assert_1.strict)((0, internal_1.isFluidHandlePayloadPending)(handle1));
                    (0, node_assert_1.strict)((0, internal_1.isFluidHandlePayloadPending)(handle2));
                    node_assert_1.strict.strictEqual(handle1.payloadState, "shared", "Payload should be shared");
                    node_assert_1.strict.strictEqual(handle2.payloadState, "shared", "Payload should be shared");
                    const { ids, redirectTable } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    node_assert_1.strict.strictEqual(ids?.length, 2);
                    node_assert_1.strict.strictEqual(redirectTable?.length, 2);
                });
            });
            describe("Abort", () => {
                it("Can abort before blob upload", async () => {
                    const { blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({ createBlobPayloadPending });
                    const ac = new AbortController();
                    ac.abort("abort test");
                    const createP = blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"), ac.signal);
                    await node_assert_1.strict.rejects(createBlobPayloadPending ? (0, blobTestUtils_js_1.ensureBlobsShared)([await createP]) : createP, {
                        message: "uploadBlob aborted",
                    });
                    const { ids, redirectTable } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    node_assert_1.strict.strictEqual(ids, undefined);
                    node_assert_1.strict.strictEqual(redirectTable, undefined);
                });
                it("Can abort during blob upload", async () => {
                    const { mockBlobStorage, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        createBlobPayloadPending,
                    });
                    mockBlobStorage.pause();
                    const ac = new AbortController();
                    const createP = blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"), ac.signal);
                    if (createBlobPayloadPending) {
                        const handle = await createP;
                        (0, blobTestUtils_js_1.attachHandle)(handle);
                    }
                    ac.abort("abort test");
                    await node_assert_1.strict.rejects(createBlobPayloadPending ? (0, blobTestUtils_js_1.ensureBlobsShared)([await createP]) : createP, {
                        message: "uploadBlob aborted",
                    });
                    const { ids, redirectTable } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    node_assert_1.strict.strictEqual(ids, undefined);
                    node_assert_1.strict.strictEqual(redirectTable, undefined);
                });
                it("Can abort before failed blob upload", async () => {
                    const { mockBlobStorage, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        createBlobPayloadPending,
                    });
                    mockBlobStorage.pause();
                    const ac = new AbortController();
                    const createP1 = blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"), ac.signal);
                    const createP2 = blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("world"));
                    if (createBlobPayloadPending) {
                        const handle1 = await createP1;
                        const handle2 = await createP2;
                        (0, blobTestUtils_js_1.attachHandle)(handle1);
                        (0, blobTestUtils_js_1.attachHandle)(handle2);
                    }
                    ac.abort("abort test");
                    await node_assert_1.strict.rejects(createBlobPayloadPending ? (0, blobTestUtils_js_1.ensureBlobsShared)([await createP1]) : createP1, {
                        message: "uploadBlob aborted",
                    });
                    await mockBlobStorage.waitCreateOne({ error: new Error("fake driver error") });
                    await mockBlobStorage.waitCreateOne({ error: new Error("fake driver error") });
                    await node_assert_1.strict.rejects(createBlobPayloadPending ? (0, blobTestUtils_js_1.ensureBlobsShared)([await createP2]) : createP2, {
                        message: "fake driver error",
                    });
                    const { ids, redirectTable } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    node_assert_1.strict.strictEqual(ids, undefined);
                    node_assert_1.strict.strictEqual(redirectTable, undefined);
                });
                it("Can abort while blob attach message is in flight", async () => {
                    const { mockOrderingService, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        createBlobPayloadPending,
                    });
                    mockOrderingService.pause();
                    const ac = new AbortController();
                    const createP = blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"), ac.signal);
                    if (createBlobPayloadPending) {
                        const handle = await createP;
                        (0, blobTestUtils_js_1.attachHandle)(handle);
                    }
                    // Wait for the message to be sent
                    await new Promise((resolve) => {
                        if (mockOrderingService.messagesReceived !== 1) {
                            const onMessageReceived = () => {
                                if (mockOrderingService.messagesReceived === 1) {
                                    resolve();
                                    mockOrderingService.events.off("messageReceived", onMessageReceived);
                                }
                            };
                            mockOrderingService.events.on("messageReceived", onMessageReceived);
                        }
                    });
                    ac.abort("abort test");
                    await node_assert_1.strict.rejects(createBlobPayloadPending ? (0, blobTestUtils_js_1.ensureBlobsShared)([await createP]) : createP, {
                        message: "uploadBlob aborted",
                    });
                    // Also verify that BlobManager doesn't resubmit the message for an already-aborted blob
                    await mockOrderingService.waitDropOne();
                    node_assert_1.strict.strictEqual(mockOrderingService.messagesReceived, 1, "Shouldn't have sent more messages");
                    const { ids, redirectTable } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    node_assert_1.strict.strictEqual(ids, undefined);
                    node_assert_1.strict.strictEqual(redirectTable, undefined);
                });
                it("Abort does nothing after blob creation succeeds", async () => {
                    const { blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({ createBlobPayloadPending });
                    const ac = new AbortController();
                    const handle = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"), ac.signal);
                    if (createBlobPayloadPending) {
                        await (0, blobTestUtils_js_1.ensureBlobsShared)([handle]);
                    }
                    ac.abort("abort test");
                    const { ids, redirectTable } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    node_assert_1.strict.strictEqual(ids?.length, 1);
                    node_assert_1.strict.strictEqual(redirectTable?.length, 1);
                });
            });
            describe("getPendingBlobs", () => { });
        });
        // #region Summaries
        describe("Summaries", () => {
            describe("Generating summaries", () => {
                it("Empty summary", () => {
                    const { blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({ createBlobPayloadPending });
                    const { ids, redirectTable } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    node_assert_1.strict.strictEqual(ids, undefined, "Shouldn't have ids for empty summary");
                    node_assert_1.strict.strictEqual(redirectTable, undefined, "Shouldn't have redirectTable for empty summary");
                });
                it("Detached, non-dedupe storage", async () => {
                    const { blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        attached: false,
                        createBlobPayloadPending,
                    });
                    await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("world"));
                    await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("world"));
                    // Note that this summary is not generated in normal usage, it's just a means to
                    // validate what is being put into the detached storage. In normal use we'd see
                    // a BlobManager.patchRedirectTable() call before being asked to produce a summary.
                    const { ids, redirectTable } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    node_assert_1.strict.strictEqual(ids?.length, 3);
                    node_assert_1.strict.strictEqual(redirectTable?.length, 3);
                });
                it("Attached, dedupe storage", async () => {
                    const { blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        createBlobPayloadPending,
                    });
                    const handle1 = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    const handle2 = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("world"));
                    const handle3 = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("world"));
                    // Ensure the blobs are attached so they are included in the summary.
                    if (createBlobPayloadPending) {
                        await (0, blobTestUtils_js_1.ensureBlobsShared)([handle1, handle2, handle3]);
                    }
                    const { ids, redirectTable } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    node_assert_1.strict.strictEqual(ids?.length, 2);
                    node_assert_1.strict.strictEqual(redirectTable?.length, 3);
                });
                it("Detached -> attached, deduping after attach", async () => {
                    const { mockBlobStorage, mockRuntime, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        attached: false,
                        createBlobPayloadPending,
                    });
                    await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("world"));
                    await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("world"));
                    await (0, blobTestUtils_js_1.simulateAttach)(mockBlobStorage, mockRuntime, blobManager);
                    const { ids: ids1, redirectTable: redirectTable1 } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    // As attach uploads the non-deduped blobs to the deduping storage, the duplicate
                    // "world" will remain in the redirectTable (since it has a unique localId), but
                    // its attachment will be deduplicated.
                    node_assert_1.strict.strictEqual(ids1?.length, 2);
                    node_assert_1.strict.strictEqual(redirectTable1?.length, 3);
                    const handle1 = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    const handle2 = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("world"));
                    const handle3 = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("another"));
                    const handle4 = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("another"));
                    // Ensure the blobs are attached so they are included in the summary.
                    if (createBlobPayloadPending) {
                        await (0, blobTestUtils_js_1.ensureBlobsShared)([handle1, handle2, handle3, handle4]);
                    }
                    const { ids: ids2, redirectTable: redirectTable2 } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    node_assert_1.strict.strictEqual(ids2?.length, 3);
                    node_assert_1.strict.strictEqual(redirectTable2?.length, 7);
                });
            });
            describe("Loading from summaries", () => {
                it("Can load from a summary and retrieve its blobs", async () => {
                    const { mockBlobStorage, mockOrderingService, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        createBlobPayloadPending,
                    });
                    const handle1 = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    const handle2 = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("world"));
                    const { localId: localId1 } = (0, blobTestUtils_js_1.unpackHandle)(handle1);
                    const { localId: localId2 } = (0, blobTestUtils_js_1.unpackHandle)(handle2);
                    if (createBlobPayloadPending) {
                        await (0, blobTestUtils_js_1.ensureBlobsShared)([handle1, handle2]);
                    }
                    const blobManagerLoadInfo = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                    const { ids: ids1, redirectTable: redirectTable1 } = blobManagerLoadInfo;
                    const { blobManager: blobManager2 } = (0, blobTestUtils_js_1.createTestMaterial)({
                        mockBlobStorage,
                        mockOrderingService,
                        blobManagerLoadInfo,
                        createBlobPayloadPending,
                    });
                    (0, node_assert_1.strict)(blobManager2.hasBlob(localId1));
                    (0, node_assert_1.strict)(blobManager2.hasBlob(localId2));
                    const blob1FromManager2 = await blobManager2.getBlob(localId1, createBlobPayloadPending);
                    node_assert_1.strict.strictEqual((0, blobTestUtils_js_1.blobToText)(blob1FromManager2), "hello", "Blob content mismatch");
                    const blob2FromManager2 = await blobManager2.getBlob(localId2, createBlobPayloadPending);
                    node_assert_1.strict.strictEqual((0, blobTestUtils_js_1.blobToText)(blob2FromManager2), "world", "Blob content mismatch");
                    // Verify that resummarizing gives the same results
                    const { ids: ids2, redirectTable: redirectTable2 } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager2);
                    node_assert_1.strict.deepStrictEqual(ids2, ids1, "IDs mismatch");
                    node_assert_1.strict.deepStrictEqual(redirectTable2, redirectTable1, "Redirect table mismatch");
                });
                it("Can load from a summary with only ids and retrieve blobs using remoteId", async () => {
                    const { mockBlobStorage, mockOrderingService, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                        createBlobPayloadPending,
                    });
                    const handle = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                    if (createBlobPayloadPending) {
                        await (0, blobTestUtils_js_1.ensureBlobsShared)([handle]);
                    }
                    // This is stripping out the redirectTable from the summary, to simulate an older
                    // BlobManager behavior that blobs created after attach would only be represented in the
                    // attachments and not in the redirectTable.
                    const blobManagerLoadInfo = {
                        ids: (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager).ids,
                    };
                    const remoteId = blobManagerLoadInfo.ids?.[0];
                    (0, node_assert_1.strict)(remoteId !== undefined, "Should have one attachment");
                    const { blobManager: blobManager2 } = (0, blobTestUtils_js_1.createTestMaterial)({
                        mockBlobStorage,
                        mockOrderingService,
                        blobManagerLoadInfo,
                        createBlobPayloadPending,
                    });
                    // That older behavior made the blob retrievable using its remoteId directly.
                    (0, node_assert_1.strict)(blobManager2.hasBlob(remoteId));
                    const blobFromManager2 = await blobManager2.getBlob(remoteId, createBlobPayloadPending);
                    node_assert_1.strict.strictEqual((0, blobTestUtils_js_1.blobToText)(blobFromManager2), "hello", "Blob content mismatch");
                    // The identity redirectTable entry should be stripped out of the summary, but the
                    // attachment should remain.
                    const { ids: ids2, redirectTable: redirectTable2 } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager2);
                    node_assert_1.strict.strictEqual(ids2?.length, 1);
                    node_assert_1.strict.strictEqual(redirectTable2, undefined);
                });
            });
        });
        // #region GC
        describe("Garbage collection", () => {
            it("Errors when trying to get a deleted blob", async () => {
                const { mockGarbageCollector, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                    createBlobPayloadPending,
                });
                const handle = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                if (createBlobPayloadPending) {
                    await (0, blobTestUtils_js_1.ensureBlobsShared)([handle]);
                }
                const { localId } = (0, blobTestUtils_js_1.unpackHandle)(handle);
                mockGarbageCollector.simulateBlobDeletion((0, index_js_1.getGCNodePathFromLocalId)(localId));
                await node_assert_1.strict.rejects(blobManager.getBlob(localId, createBlobPayloadPending), {
                    message: `Blob was deleted: ${localId}`,
                    code: 404,
                });
            });
            it("Deletes unused blobs", async () => {
                const { blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                    createBlobPayloadPending,
                });
                const handle1 = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                const { localId: localId1 } = (0, blobTestUtils_js_1.unpackHandle)(handle1);
                if (createBlobPayloadPending) {
                    await (0, blobTestUtils_js_1.ensureBlobsShared)([handle1]);
                }
                const { ids: ids1, redirectTable: redirectTable1 } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                node_assert_1.strict.strictEqual(ids1?.length, 1);
                node_assert_1.strict.strictEqual(redirectTable1?.length, 1);
                blobManager.deleteSweepReadyNodes([(0, index_js_1.getGCNodePathFromLocalId)(localId1)]);
                const { ids: ids2, redirectTable: redirectTable2 } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                node_assert_1.strict.strictEqual(ids2, undefined);
                node_assert_1.strict.strictEqual(redirectTable2, undefined);
            });
            it("Deletes unused blobs only after all duplicates are deleted", async () => {
                const { blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                    createBlobPayloadPending,
                });
                // These blobs will be deduped against each other
                const handle1 = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                const handle2 = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                const { localId: localId1 } = (0, blobTestUtils_js_1.unpackHandle)(handle1);
                const { localId: localId2 } = (0, blobTestUtils_js_1.unpackHandle)(handle2);
                if (createBlobPayloadPending) {
                    await (0, blobTestUtils_js_1.ensureBlobsShared)([handle1, handle2]);
                }
                const { ids: ids1, redirectTable: redirectTable1 } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                node_assert_1.strict.strictEqual(ids1?.length, 1);
                node_assert_1.strict.strictEqual(redirectTable1?.length, 2);
                blobManager.deleteSweepReadyNodes([(0, index_js_1.getGCNodePathFromLocalId)(localId1)]);
                const { ids: ids2, redirectTable: redirectTable2 } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                node_assert_1.strict.strictEqual(ids2?.length, 1);
                node_assert_1.strict.strictEqual(redirectTable2?.length, 1);
                blobManager.deleteSweepReadyNodes([(0, index_js_1.getGCNodePathFromLocalId)(localId2)]);
                const { ids: ids3, redirectTable: redirectTable3 } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                node_assert_1.strict.strictEqual(ids3, undefined);
                node_assert_1.strict.strictEqual(redirectTable3, undefined);
            });
        });
        // #region Storage ID lookup
        describe("Storage ID lookup", () => {
            it("lookupTemporaryBlobStorageId returns correct storage ID for attached blobs", async () => {
                const { blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                    createBlobPayloadPending,
                });
                const handle = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                await (0, blobTestUtils_js_1.ensureBlobsShared)([handle]);
                const { localId } = (0, blobTestUtils_js_1.unpackHandle)(handle);
                const { ids } = (0, blobTestUtils_js_1.getSummaryContentsWithFormatValidation)(blobManager);
                const expectedStorageId = ids?.[0];
                (0, node_assert_1.strict)(expectedStorageId !== undefined, "Storage id not found in summary");
                const foundStorageId = blobManager.lookupTemporaryBlobStorageId(localId);
                node_assert_1.strict.strictEqual(foundStorageId, expectedStorageId, "Storage ID should match expected value");
            });
            it("lookupTemporaryBlobStorageId returns undefined for pending blobs", async function () {
                if (!createBlobPayloadPending) {
                    // This test only applies when payload pending is enabled
                    this.skip();
                }
                const { mockBlobStorage, mockOrderingService, blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                    createBlobPayloadPending,
                });
                // Pause blob storage and ordering service so the upload will remain pending
                mockBlobStorage.pause();
                mockOrderingService.pause();
                // Create a blob and attach the handle to start the upload attempt
                const handle = await blobManager.createBlob((0, blobTestUtils_js_1.textToBlob)("hello"));
                (0, blobTestUtils_js_1.attachHandle)(handle);
                const { localId } = (0, blobTestUtils_js_1.unpackHandle)(handle);
                // The blob upload should be pending, so lookupTemporaryBlobStorageId should return undefined
                const storageId1 = blobManager.lookupTemporaryBlobStorageId(localId);
                node_assert_1.strict.strictEqual(storageId1, undefined, "Storage ID should be undefined while blob upload pending");
                // Allow just the blob upload to process, but not the attach message
                await mockBlobStorage.waitCreateOne();
                const storageId2 = blobManager.lookupTemporaryBlobStorageId(localId);
                node_assert_1.strict.strictEqual(storageId2, undefined, "Storage ID should be undefined while blob attach message pending");
                // Now allow the attach message to be sequenced
                await mockOrderingService.waitSequenceOne();
                await (0, blobTestUtils_js_1.ensureBlobsShared)([handle]);
                const storageIdAfterProcessing = blobManager.lookupTemporaryBlobStorageId(localId);
                (0, node_assert_1.strict)(storageIdAfterProcessing !== undefined, "Storage ID should be found after processing");
            });
            it("lookupTemporaryBlobStorageId returns undefined for unknown blob ID", () => {
                const { blobManager } = (0, blobTestUtils_js_1.createTestMaterial)({
                    createBlobPayloadPending,
                });
                const unknownId = "unknown-blob-id";
                const storageId = blobManager.lookupTemporaryBlobStorageId(unknownId);
                node_assert_1.strict.strictEqual(storageId, undefined, "Storage ID should be undefined for unknown blob ID");
            });
        });
    });
}
//# sourceMappingURL=blobManager.spec.js.map