@fluidframework/container-loader 2.100.0 → 2.101.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/api-report/container-loader.legacy.alpha.api.md +13 -1
  3. package/dist/captureReferencedContents.d.ts +154 -0
  4. package/dist/captureReferencedContents.d.ts.map +1 -0
  5. package/dist/captureReferencedContents.js +349 -0
  6. package/dist/captureReferencedContents.js.map +1 -0
  7. package/dist/connectionManager.d.ts.map +1 -1
  8. package/dist/connectionManager.js +25 -7
  9. package/dist/connectionManager.js.map +1 -1
  10. package/dist/connectionStateHandler.d.ts.map +1 -1
  11. package/dist/connectionStateHandler.js +3 -1
  12. package/dist/connectionStateHandler.js.map +1 -1
  13. package/dist/container.d.ts.map +1 -1
  14. package/dist/container.js +6 -1
  15. package/dist/container.js.map +1 -1
  16. package/dist/containerStorageAdapter.d.ts +19 -1
  17. package/dist/containerStorageAdapter.d.ts.map +1 -1
  18. package/dist/containerStorageAdapter.js.map +1 -1
  19. package/dist/createAndLoadContainerUtils.d.ts +95 -0
  20. package/dist/createAndLoadContainerUtils.d.ts.map +1 -1
  21. package/dist/createAndLoadContainerUtils.js +137 -11
  22. package/dist/createAndLoadContainerUtils.js.map +1 -1
  23. package/dist/frozenServices.d.ts +113 -30
  24. package/dist/frozenServices.d.ts.map +1 -1
  25. package/dist/frozenServices.js +236 -58
  26. package/dist/frozenServices.js.map +1 -1
  27. package/dist/index.d.ts +2 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +5 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/legacyAlpha.d.ts +2 -0
  32. package/dist/loaderLayerCompatState.d.ts +1 -1
  33. package/dist/packageVersion.d.ts +1 -1
  34. package/dist/packageVersion.js +1 -1
  35. package/dist/packageVersion.js.map +1 -1
  36. package/dist/pendingLocalStateStore.d.ts.map +1 -1
  37. package/dist/pendingLocalStateStore.js +9 -3
  38. package/dist/pendingLocalStateStore.js.map +1 -1
  39. package/dist/serializedStateManager.d.ts +16 -1
  40. package/dist/serializedStateManager.d.ts.map +1 -1
  41. package/dist/serializedStateManager.js +11 -1
  42. package/dist/serializedStateManager.js.map +1 -1
  43. package/lib/captureReferencedContents.d.ts +154 -0
  44. package/lib/captureReferencedContents.d.ts.map +1 -0
  45. package/lib/captureReferencedContents.js +338 -0
  46. package/lib/captureReferencedContents.js.map +1 -0
  47. package/lib/connectionManager.d.ts.map +1 -1
  48. package/lib/connectionManager.js +26 -8
  49. package/lib/connectionManager.js.map +1 -1
  50. package/lib/connectionStateHandler.d.ts.map +1 -1
  51. package/lib/connectionStateHandler.js +3 -1
  52. package/lib/connectionStateHandler.js.map +1 -1
  53. package/lib/container.d.ts.map +1 -1
  54. package/lib/container.js +6 -1
  55. package/lib/container.js.map +1 -1
  56. package/lib/containerStorageAdapter.d.ts +19 -1
  57. package/lib/containerStorageAdapter.d.ts.map +1 -1
  58. package/lib/containerStorageAdapter.js.map +1 -1
  59. package/lib/createAndLoadContainerUtils.d.ts +95 -0
  60. package/lib/createAndLoadContainerUtils.d.ts.map +1 -1
  61. package/lib/createAndLoadContainerUtils.js +128 -3
  62. package/lib/createAndLoadContainerUtils.js.map +1 -1
  63. package/lib/frozenServices.d.ts +113 -30
  64. package/lib/frozenServices.d.ts.map +1 -1
  65. package/lib/frozenServices.js +233 -57
  66. package/lib/frozenServices.js.map +1 -1
  67. package/lib/index.d.ts +2 -1
  68. package/lib/index.d.ts.map +1 -1
  69. package/lib/index.js +2 -1
  70. package/lib/index.js.map +1 -1
  71. package/lib/legacyAlpha.d.ts +2 -0
  72. package/lib/loaderLayerCompatState.d.ts +1 -1
  73. package/lib/packageVersion.d.ts +1 -1
  74. package/lib/packageVersion.js +1 -1
  75. package/lib/packageVersion.js.map +1 -1
  76. package/lib/pendingLocalStateStore.d.ts.map +1 -1
  77. package/lib/pendingLocalStateStore.js +9 -3
  78. package/lib/pendingLocalStateStore.js.map +1 -1
  79. package/lib/serializedStateManager.d.ts +16 -1
  80. package/lib/serializedStateManager.d.ts.map +1 -1
  81. package/lib/serializedStateManager.js +11 -1
  82. package/lib/serializedStateManager.js.map +1 -1
  83. package/package.json +11 -11
  84. package/src/captureReferencedContents.ts +446 -0
  85. package/src/connectionManager.ts +30 -8
  86. package/src/connectionStateHandler.ts +14 -9
  87. package/src/container.ts +6 -0
  88. package/src/containerStorageAdapter.ts +20 -1
  89. package/src/createAndLoadContainerUtils.ts +229 -2
  90. package/src/frozenServices.ts +285 -64
  91. package/src/index.ts +7 -0
  92. package/src/packageVersion.ts +1 -1
  93. package/src/pendingLocalStateStore.ts +8 -1
  94. package/src/serializedStateManager.ts +28 -1
@@ -7,49 +7,132 @@ import type { IDisposable } from "@fluidframework/core-interfaces";
7
7
  import { type ConnectionMode, type IClientConfiguration, type IDocumentDeltaConnection, type IDocumentDeltaConnectionEvents, type IDocumentMessage, type IDocumentService, type IDocumentServiceFactory, type IResolvedUrl, type ISequencedDocumentMessage, type ISignalClient, type ISignalMessage, type ITokenClaims } from "@fluidframework/driver-definitions/internal";
8
8
  import type { IConnectionStateChangeReason } from "./contracts.js";
9
9
  /**
10
- * Creation of a FrozenDocumentServiceFactory which wraps an existing
11
- * DocumentServiceFactory to provide a storage-only document service.
10
+ * Creates an `IDocumentServiceFactory` that produces a "frozen" document service: one whose
11
+ * delta stream never sends or receives ops, and whose storage service only supports
12
+ * `IDocumentStorageService.readBlob`. Used to load a container from pending local state
13
+ * without re-establishing a live connection.
12
14
  *
13
- * @param documentServiceFactory - The underlying DocumentServiceFactory to wrap.
14
- * @returns A FrozenDocumentServiceFactory
15
+ * @param factory - The underlying factory to wrap. Its storage backs blob reads; all other
16
+ * storage operations throw. May be omitted when blob fetches are not required.
17
+ * @param readOnly - When `true` (the default), the document service advertises the
18
+ * `IDocumentServicePolicies.storageOnly` policy, which causes the loader to surface the
19
+ * container as read-only (see `IContainer.readOnlyInfo`).
20
+ *
21
+ * When `false`, the container is loaded as writable so the runtime will accept DDS submissions.
22
+ * The connection itself stays `Connected`: `ConnectionManager.sendMessages` recognizes the
23
+ * `WritableFrozenDeltaStream` as the live connection and short-circuits — the message is dropped
24
+ * at the network layer rather than triggering a read→write reconnect. Local DDS state continues
25
+ * to update via optimistic apply, and submitted ops accumulate in the runtime's pending-state
26
+ * manager, which is exactly the state needed to capture pending local state. Use `false` when
27
+ * callers want to accrue and capture pending state without publishing it.
28
+ * @returns A factory that produces frozen document services.
15
29
  * @legacy @alpha
16
30
  */
17
- export declare function createFrozenDocumentServiceFactory(factory?: IDocumentServiceFactory | Promise<IDocumentServiceFactory>): IDocumentServiceFactory;
31
+ export declare function createFrozenDocumentServiceFactory(factory?: IDocumentServiceFactory | Promise<IDocumentServiceFactory>, readOnly?: boolean): IDocumentServiceFactory;
18
32
  export declare class FrozenDocumentServiceFactory implements IDocumentServiceFactory {
19
- private readonly documentServiceFactory?;
20
- constructor(documentServiceFactory?: IDocumentServiceFactory | Promise<IDocumentServiceFactory> | undefined);
33
+ readonly readOnly: boolean;
34
+ readonly inner?: IDocumentServiceFactory | Promise<IDocumentServiceFactory> | undefined;
35
+ constructor(readOnly: boolean, inner?: IDocumentServiceFactory | Promise<IDocumentServiceFactory> | undefined);
21
36
  createDocumentService(resolvedUrl: IResolvedUrl): Promise<IDocumentService>;
22
37
  createContainer(): Promise<IDocumentService>;
23
38
  }
24
39
  /**
25
- * Implementation of IDocumentDeltaConnection that does not support submitting
26
- * or receiving ops. Used in storage-only mode and in frozen loads.
40
+ * Inert `IDocumentDeltaConnection` for frozen container loads. Has no server upstream:
41
+ * op and signal streams are empty, and `initialClients` contains only its own synthetic
42
+ * read-only client — which lets the connection state handler observe "self" in the audience
43
+ * and transition the container to Connected without waiting for a real join op or signal.
44
+ *
45
+ * Two concrete variants share this base — see their JSDoc for variant-specific details:
46
+ *
47
+ * - {@link FrozenDeltaStream} — read-only.
48
+ * - {@link WritableFrozenDeltaStream} — writable.
49
+ *
50
+ * Both variants nack any incoming `submit`: this connection has no upstream and
51
+ * `ConnectionManager.sendMessages` recognizes `WritableFrozenDeltaStream` and drops messages
52
+ * before they reach `submit`, so under normal flow it should never fire. A nack reaching the
53
+ * connectionManager surfaces the misuse — and may close the container — which is the right
54
+ * defensive signal that something has bypassed the expected flow.
55
+ *
56
+ * `submitSignal` is a silent no-op for both variants. Signals are ephemeral and best-effort —
57
+ * runtime/presence subsystems may submit them at any point in the writable-frozen lifetime, and
58
+ * dropping them is the correct behavior here (we have no upstream). Closing the container or
59
+ * triggering a reconnect on a stray signal would be strictly worse than dropping it.
27
60
  */
28
- export declare class FrozenDeltaStream extends TypedEventEmitter<IDocumentDeltaConnectionEvents> implements IDocumentDeltaConnection, IDisposable {
29
- readonly storageOnlyReason?: string | undefined;
30
- readonly readonlyConnectionReason?: IConnectionStateChangeReason<import("@fluidframework/core-interfaces").IErrorBase> | undefined;
31
- clientId: string;
32
- claims: ITokenClaims;
33
- mode: ConnectionMode;
34
- existing: boolean;
35
- maxMessageSize: number;
36
- version: string;
37
- initialMessages: ISequencedDocumentMessage[];
38
- initialSignals: ISignalMessage[];
39
- initialClients: ISignalClient[];
40
- serviceConfiguration: IClientConfiguration;
41
- checkpointSequenceNumber?: number | undefined;
42
- /**
43
- * Connection which is not connected to socket.
44
- * @param storageOnlyReason - Reason on why the connection to delta stream is not allowed.
45
- * @param readonlyConnectionReason - reason/error if any which lead to using FrozenDeltaStream.
46
- */
47
- constructor(storageOnlyReason?: string | undefined, readonlyConnectionReason?: IConnectionStateChangeReason<import("@fluidframework/core-interfaces").IErrorBase> | undefined);
61
+ declare abstract class FrozenDeltaStreamBase extends TypedEventEmitter<IDocumentDeltaConnectionEvents> implements IDocumentDeltaConnection, IDisposable {
62
+ readonly clientId: string;
63
+ readonly claims: ITokenClaims;
64
+ readonly initialClients: ISignalClient[];
65
+ readonly mode: ConnectionMode;
66
+ readonly existing: boolean;
67
+ readonly maxMessageSize: number;
68
+ readonly version: string;
69
+ readonly initialMessages: ISequencedDocumentMessage[];
70
+ readonly initialSignals: ISignalMessage[];
71
+ readonly serviceConfiguration: IClientConfiguration;
72
+ readonly checkpointSequenceNumber?: number | undefined;
73
+ constructor(clientId: string, claims: ITokenClaims);
48
74
  submit(messages: IDocumentMessage[]): void;
49
- submitSignal(message: unknown): void;
75
+ submitSignal(_message: unknown): void;
50
76
  private _disposed;
51
77
  get disposed(): boolean;
52
78
  dispose(): void;
53
79
  }
80
+ /**
81
+ * Read-only variant of {@link FrozenDeltaStreamBase}. Claims show only `DocRead`. Used by
82
+ * storage-only loads (where `connectionManager` synthesizes one directly via
83
+ * `policies.storageOnly`) and by the forbidden / out-of-storage fallback paths.
84
+ * {@link isFrozenDeltaStreamConnection} matches this variant and drives the read-only forcing
85
+ * in `ConnectionManager.readOnlyInfo`. Uses the historical `"storage-only client"` constant
86
+ * `clientId`, preserving existing behavior for any consumer that keys off it.
87
+ *
88
+ * `storageOnlyReason` and `readonlyConnectionReason` are surfaced through `IContainer.readOnlyInfo`
89
+ * for diagnostics on the fallback paths (`isDeltaStreamConnectionForbiddenError`,
90
+ * `outOfStorageError`).
91
+ */
92
+ export declare class FrozenDeltaStream extends FrozenDeltaStreamBase {
93
+ readonly storageOnlyReason: string | undefined;
94
+ readonly readonlyConnectionReason: IConnectionStateChangeReason | undefined;
95
+ constructor(options?: {
96
+ storageOnlyReason?: string;
97
+ readonlyConnectionReason?: IConnectionStateChangeReason;
98
+ });
99
+ }
100
+ /**
101
+ * Variant of {@link FrozenDeltaStreamBase} that appears to support writing but remains
102
+ * "frozen" — no messages are actually sent or received. The stream itself does not enforce
103
+ * the no-send guarantee; that lives in `ConnectionManager.sendMessages`, which recognizes
104
+ * any `WritableFrozenDeltaStream` (via {@link isWritableFrozenDeltaStreamConnection}) and
105
+ * short-circuits before its read-mode upgrade branch. Submitted ops are dropped at the
106
+ * connection-manager layer, so the container stays `Connected` and the runtime accumulates
107
+ * them in `pendingStateManager`.
108
+ *
109
+ * "Appears writable" mechanics: claims include `DocWrite` so the container surfaces as
110
+ * writable; not matched by {@link isFrozenDeltaStreamConnection}, so `readOnlyInfo` reports
111
+ * `readonly: false`. Connection mode stays `"read"` (advertising `"write"` would imply quorum
112
+ * membership we cannot honor).
113
+ *
114
+ * Each instance mints a fresh `frozen-delta-stream/<uuid>` `clientId` to avoid
115
+ * `pendingStateManager` `0x173` (`replayPendingStates called twice for same clientId!`) on
116
+ * reconnect with dirty pending ops. Sibling (not subclass) of `FrozenDeltaStream` so
117
+ * `instanceof` cleanly distinguishes the two for `ConnectionManager`'s short-circuits.
118
+ */
119
+ export declare class WritableFrozenDeltaStream extends FrozenDeltaStreamBase {
120
+ constructor();
121
+ }
122
+ /**
123
+ * Recognizes the read-only variant of {@link FrozenDeltaStreamBase}. Drives the storage-only
124
+ * forcing in `ConnectionManager.readOnlyInfo`: only the read-only variant should make the
125
+ * container surface as read-only. {@link WritableFrozenDeltaStream} is a sibling class, not
126
+ * a subclass, so `instanceof FrozenDeltaStream` already excludes it.
127
+ */
54
128
  export declare function isFrozenDeltaStreamConnection(connection: unknown): connection is FrozenDeltaStream;
129
+ /**
130
+ * Recognizes the writable variant of {@link FrozenDeltaStreamBase}. Drives the
131
+ * `ConnectionManager.sendMessages` short-circuit: writable-frozen submits must be dropped at
132
+ * the network layer instead of triggering a read→write reconnect. Sibling (not subclass) of
133
+ * {@link FrozenDeltaStream}, so `instanceof WritableFrozenDeltaStream` excludes the read-only
134
+ * variant.
135
+ */
136
+ export declare function isWritableFrozenDeltaStreamConnection(connection: unknown): connection is WritableFrozenDeltaStream;
137
+ export {};
55
138
  //# sourceMappingURL=frozenServices.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"frozenServices.d.ts","sourceRoot":"","sources":["../src/frozenServices.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAEnE,OAAO,EAEN,KAAK,cAAc,EAEnB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,8BAA8B,EAEnC,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EAErB,KAAK,uBAAuB,EAG5B,KAAK,YAAY,EACjB,KAAK,yBAAyB,EAC9B,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,MAAM,6CAA6C,CAAC;AAErD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAEnE;;;;;;;GAOG;AACH,wBAAgB,kCAAkC,CACjD,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC,GAClE,uBAAuB,CAKzB;AAED,qBAAa,4BAA6B,YAAW,uBAAuB;IAE1E,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAC;gBAAvB,sBAAsB,CAAC,wEAEL;IAG9B,qBAAqB,CAAC,WAAW,EAAE,YAAY,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAU3E,eAAe,IAAI,OAAO,CAAC,gBAAgB,CAAC;CAGlD;AA8DD;;;GAGG;AACH,qBAAa,iBACZ,SAAQ,iBAAiB,CAAC,8BAA8B,CACxD,YAAW,wBAAwB,EAAE,WAAW;aA2B/B,iBAAiB,CAAC;aAClB,wBAAwB,CAAC;IA1B1C,QAAQ,SAA6B;IAErC,MAAM,eAEY;IAClB,IAAI,EAAE,cAAc,CAAU;IAC9B,QAAQ,EAAE,OAAO,CAAQ;IACzB,cAAc,EAAE,MAAM,CAAK;IAC3B,OAAO,EAAE,MAAM,CAAM;IACrB,eAAe,EAAE,yBAAyB,EAAE,CAAM;IAClD,cAAc,EAAE,cAAc,EAAE,CAAM;IACtC,cAAc,EAAE,aAAa,EAAE,CAE7B;IACF,oBAAoB,EAAE,oBAAoB,CAGxC;IACF,wBAAwB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAa;IAC1D;;;;OAIG;gBAEc,iBAAiB,CAAC,oBAAQ,EAC1B,wBAAwB,CAAC,gGAA8B;IAIxE,MAAM,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,IAAI;IAY1C,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAOpC,OAAO,CAAC,SAAS,CAAS;IAC1B,IAAW,QAAQ,IAAI,OAAO,CAE7B;IACM,OAAO,IAAI,IAAI;CAGtB;AACD,wBAAgB,6BAA6B,CAC5C,UAAU,EAAE,OAAO,GACjB,UAAU,IAAI,iBAAiB,CAEjC"}
1
+ {"version":3,"file":"frozenServices.d.ts","sourceRoot":"","sources":["../src/frozenServices.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAEnE,OAAO,EAEN,KAAK,cAAc,EAEnB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,8BAA8B,EAEnC,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EAErB,KAAK,uBAAuB,EAG5B,KAAK,YAAY,EACjB,KAAK,yBAAyB,EAC9B,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,MAAM,6CAA6C,CAAC;AAGrD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAEnE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kCAAkC,CACjD,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,uBAAuB,CAAC,EACpE,QAAQ,GAAE,OAAc,GACtB,uBAAuB,CAUzB;AAED,qBAAa,4BAA6B,YAAW,uBAAuB;aAE1D,QAAQ,EAAE,OAAO;aACjB,KAAK,CAAC;gBADN,QAAQ,EAAE,OAAO,EACjB,KAAK,CAAC,wEAA4D;IAG7E,qBAAqB,CAAC,WAAW,EAAE,YAAY,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAQ3E,eAAe,IAAI,OAAO,CAAC,gBAAgB,CAAC;CAGlD;AAoLD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,uBAAe,qBACd,SAAQ,iBAAiB,CAAC,8BAA8B,CACxD,YAAW,wBAAwB,EAAE,WAAW;IAEhD,SAAgB,QAAQ,EAAE,MAAM,CAAC;IACjC,SAAgB,MAAM,EAAE,YAAY,CAAC;IACrC,SAAgB,cAAc,EAAE,aAAa,EAAE,CAAC;IAChD,SAAgB,IAAI,EAAE,cAAc,CAAU;IAC9C,SAAgB,QAAQ,EAAE,OAAO,CAAQ;IACzC,SAAgB,cAAc,EAAE,MAAM,CAAK;IAC3C,SAAgB,OAAO,EAAE,MAAM,CAAM;IACrC,SAAgB,eAAe,EAAE,yBAAyB,EAAE,CAAM;IAClE,SAAgB,cAAc,EAAE,cAAc,EAAE,CAAM;IACtD,SAAgB,oBAAoB,EAAE,oBAAoB,CAGxD;IACF,SAAgB,wBAAwB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAa;gBAE9D,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY;IASlD,MAAM,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,IAAI;IAa1C,YAAY,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI;IAIrC,OAAO,CAAC,SAAS,CAAS;IAC1B,IAAW,QAAQ,IAAI,OAAO,CAE7B;IACM,OAAO,IAAI,IAAI;CAGtB;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,iBAAkB,SAAQ,qBAAqB;IAC3D,SAAgB,iBAAiB,EAAE,MAAM,GAAG,SAAS,CAAC;IACtD,SAAgB,wBAAwB,EAAE,4BAA4B,GAAG,SAAS,CAAC;gBAEvE,OAAO,CAAC,EAAE;QACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,wBAAwB,CAAC,EAAE,4BAA4B,CAAC;KACxD;CASD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,yBAA0B,SAAQ,qBAAqB;;CAInE;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC5C,UAAU,EAAE,OAAO,GACjB,UAAU,IAAI,iBAAiB,CAEjC;AAED;;;;;;GAMG;AACH,wBAAgB,qCAAqC,CACpD,UAAU,EAAE,OAAO,GACjB,UAAU,IAAI,yBAAyB,CAEzC"}
@@ -4,35 +4,53 @@
4
4
  * Licensed under the MIT License.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.isFrozenDeltaStreamConnection = exports.FrozenDeltaStream = exports.FrozenDocumentServiceFactory = exports.createFrozenDocumentServiceFactory = void 0;
7
+ exports.isWritableFrozenDeltaStreamConnection = exports.isFrozenDeltaStreamConnection = exports.WritableFrozenDeltaStream = exports.FrozenDeltaStream = exports.FrozenDocumentServiceFactory = exports.createFrozenDocumentServiceFactory = void 0;
8
8
  const client_utils_1 = require("@fluid-internal/client-utils");
9
9
  const internal_1 = require("@fluidframework/core-utils/internal");
10
10
  const internal_2 = require("@fluidframework/driver-definitions/internal");
11
+ const uuid_1 = require("uuid");
11
12
  /**
12
- * Creation of a FrozenDocumentServiceFactory which wraps an existing
13
- * DocumentServiceFactory to provide a storage-only document service.
13
+ * Creates an `IDocumentServiceFactory` that produces a "frozen" document service: one whose
14
+ * delta stream never sends or receives ops, and whose storage service only supports
15
+ * `IDocumentStorageService.readBlob`. Used to load a container from pending local state
16
+ * without re-establishing a live connection.
14
17
  *
15
- * @param documentServiceFactory - The underlying DocumentServiceFactory to wrap.
16
- * @returns A FrozenDocumentServiceFactory
18
+ * @param factory - The underlying factory to wrap. Its storage backs blob reads; all other
19
+ * storage operations throw. May be omitted when blob fetches are not required.
20
+ * @param readOnly - When `true` (the default), the document service advertises the
21
+ * `IDocumentServicePolicies.storageOnly` policy, which causes the loader to surface the
22
+ * container as read-only (see `IContainer.readOnlyInfo`).
23
+ *
24
+ * When `false`, the container is loaded as writable so the runtime will accept DDS submissions.
25
+ * The connection itself stays `Connected`: `ConnectionManager.sendMessages` recognizes the
26
+ * `WritableFrozenDeltaStream` as the live connection and short-circuits — the message is dropped
27
+ * at the network layer rather than triggering a read→write reconnect. Local DDS state continues
28
+ * to update via optimistic apply, and submitted ops accumulate in the runtime's pending-state
29
+ * manager, which is exactly the state needed to capture pending local state. Use `false` when
30
+ * callers want to accrue and capture pending state without publishing it.
31
+ * @returns A factory that produces frozen document services.
17
32
  * @legacy @alpha
18
33
  */
19
- function createFrozenDocumentServiceFactory(factory) {
20
- // Sync path
21
- return factory instanceof FrozenDocumentServiceFactory
22
- ? factory
23
- : new FrozenDocumentServiceFactory(factory);
34
+ function createFrozenDocumentServiceFactory(factory, readOnly = true) {
35
+ if (factory instanceof FrozenDocumentServiceFactory) {
36
+ // Already wrapped. Reuse if readOnly matches; otherwise unwrap and rewrap so the caller's
37
+ // most recent readOnly intent wins (silently honoring caller intent rather than dropping
38
+ // the new argument).
39
+ return factory.readOnly === readOnly
40
+ ? factory
41
+ : new FrozenDocumentServiceFactory(readOnly, factory.inner);
42
+ }
43
+ return new FrozenDocumentServiceFactory(readOnly, factory);
24
44
  }
25
45
  exports.createFrozenDocumentServiceFactory = createFrozenDocumentServiceFactory;
26
46
  class FrozenDocumentServiceFactory {
27
- constructor(documentServiceFactory) {
28
- this.documentServiceFactory = documentServiceFactory;
47
+ constructor(readOnly, inner) {
48
+ this.readOnly = readOnly;
49
+ this.inner = inner;
29
50
  }
30
51
  async createDocumentService(resolvedUrl) {
31
- let factory = this.documentServiceFactory;
32
- if ((0, internal_1.isPromiseLike)(factory)) {
33
- factory = await this.documentServiceFactory;
34
- }
35
- return new FrozenDocumentService(resolvedUrl, await factory?.createDocumentService(resolvedUrl));
52
+ const factory = (0, internal_1.isPromiseLike)(this.inner) ? await this.inner : this.inner;
53
+ return new FrozenDocumentService(resolvedUrl, this.readOnly, await factory?.createDocumentService(resolvedUrl));
36
54
  }
37
55
  async createContainer() {
38
56
  throw new Error("The FrozenDocumentServiceFactory cannot be used to create containers.");
@@ -40,39 +58,117 @@ class FrozenDocumentServiceFactory {
40
58
  }
41
59
  exports.FrozenDocumentServiceFactory = FrozenDocumentServiceFactory;
42
60
  class FrozenDocumentService extends client_utils_1.TypedEventEmitter {
43
- constructor(resolvedUrl, documentService) {
61
+ constructor(resolvedUrl, readOnly, documentService) {
44
62
  super();
45
63
  this.resolvedUrl = resolvedUrl;
64
+ this.readOnly = readOnly;
46
65
  this.documentService = documentService;
47
- this.policies = {
48
- storageOnly: true,
49
- };
66
+ // Tracks every storage instance handed out by `connectToStorage` so `dispose()` can cascade
67
+ // disposal to each one (rejecting their hanging `createBlob` deferreds). A Set rather than
68
+ // a single field because `IDocumentService.connectToStorage` is a public API that can be
69
+ // called more than once — we cannot assume the Container holds a single instance.
70
+ this.storageServices = new Set();
71
+ // When readOnly, advertise the storageOnly policy. The connectionManager short-circuits
72
+ // on it: it synthesizes a FrozenDeltaStream itself and never calls
73
+ // connectToDeltaStream, and the readOnlyInfo getter forces the container to read-only
74
+ // because the live connection is a FrozenDeltaStream.
75
+ //
76
+ // Audit (2026-05-05): the only consumer of `policies.storageOnly` as a frozen-container
77
+ // signal is `ConnectionManager` (synthesizing a `FrozenDeltaStream` when set). All other
78
+ // matches in the loader/runtime/driver layers are either drivers reading their own
79
+ // policies (e.g. local-driver) or `IReadOnlyInfo.storageOnly`, which is derived from the
80
+ // live connection — not the policy. So the writable-frozen container is intentionally
81
+ // indistinguishable from a normal container at the policies layer; downstream behavior
82
+ // flows through the live `WritableFrozenDeltaStream` instead.
83
+ this.policies = readOnly ? { storageOnly: true } : {};
50
84
  }
51
85
  async connectToStorage() {
52
- return new FrozenDocumentStorageService(await this.documentService?.connectToStorage());
86
+ const storage = new FrozenDocumentStorageService(this.readOnly, await this.documentService?.connectToStorage());
87
+ this.storageServices.add(storage);
88
+ return storage;
53
89
  }
54
90
  async connectToDeltaStorage() {
55
91
  return frozenDocumentDeltaStorageService;
56
92
  }
57
- async connectToDeltaStream(client) {
58
- return new FrozenDeltaStream();
93
+ async connectToDeltaStream(_client) {
94
+ if (this.readOnly) {
95
+ // connectionManager short-circuits via policies.storageOnly before reaching here
96
+ // in the read-only path; reaching this branch indicates a non-connectionManager
97
+ // consumer or a regression of the short-circuit. Throw to surface the misuse
98
+ // rather than silently produce a working stream.
99
+ throw new Error("FrozenDocumentService is read-only; connectToDeltaStream should not be called (connectionManager short-circuits via policies.storageOnly)");
100
+ }
101
+ // Writable path: hand out a fresh WritableFrozenDeltaStream regardless of client.mode
102
+ // or whether this is the initial connect or a reconnect. The stream's own mode is
103
+ // "read" (advertising "write" would imply quorum membership we cannot honor), and
104
+ // `ConnectionManager.sendMessages` short-circuits on WritableFrozenDeltaStream so
105
+ // outbound writes never reach a real network. The per-instance clientId minted in
106
+ // FrozenDeltaStreamBase prevents pendingStateManager 0x173 on replay across reconnects.
107
+ return new WritableFrozenDeltaStream();
108
+ }
109
+ dispose(error) {
110
+ // Cascade disposal to each storage instance so any hanging `createBlob` promises (the
111
+ // writable-frozen pending-blob mechanism) reject and the BlobManager can release its
112
+ // references. Without this, hung promises remain held by BlobManager closures for the
113
+ // lifetime of the process.
114
+ for (const storage of this.storageServices) {
115
+ storage.dispose();
116
+ }
117
+ this.storageServices.clear();
118
+ // Forward disposal to the wrapped service. We own its lifetime (it was created for us
119
+ // by the wrapping factory and is never exposed to callers), so the contract from
120
+ // `IDocumentService.dispose` ("called by storage consumer when done with storage")
121
+ // applies here.
122
+ this.documentService?.dispose(error);
59
123
  }
60
- dispose() { }
61
124
  }
62
125
  const frozenDocumentStorageServiceHandler = () => {
63
126
  throw new Error("Operations are not supported on the FrozenDocumentStorageService.");
64
127
  };
65
128
  class FrozenDocumentStorageService {
66
- constructor(documentStorageService) {
129
+ get disposed() {
130
+ return this._disposed;
131
+ }
132
+ constructor(readOnly, documentStorageService) {
67
133
  this.documentStorageService = documentStorageService;
134
+ this._disposed = false;
68
135
  this.getSnapshotTree = frozenDocumentStorageServiceHandler;
69
136
  this.getSnapshot = frozenDocumentStorageServiceHandler;
70
137
  this.getVersions = frozenDocumentStorageServiceHandler;
71
- this.createBlob = frozenDocumentStorageServiceHandler;
72
138
  this.readBlob = this.documentStorageService?.readBlob.bind(this.documentStorageService) ??
73
139
  frozenDocumentStorageServiceHandler;
74
140
  this.uploadSummaryWithContext = frozenDocumentStorageServiceHandler;
75
141
  this.downloadSummary = frozenDocumentStorageServiceHandler;
142
+ let rejectFn;
143
+ const promise = new Promise((_, reject) => {
144
+ rejectFn = reject;
145
+ });
146
+ // Attach a no-op catch so node doesn't log an unhandled-rejection warning when
147
+ // dispose runs before any caller has awaited the promise. Callers awaiting the
148
+ // original promise still observe the rejection.
149
+ promise.catch(() => { });
150
+ this.disposalDeferred = { promise, reject: rejectFn };
151
+ // In the writable-frozen path, `createBlob` returns a never-resolving promise instead
152
+ // of throwing. This keeps the BlobManager's `localBlobCache` entry in the `uploading`
153
+ // state: `getPendingBlobs` downgrades `uploading` blobs to `localOnly` in pending
154
+ // state, so the blob survives `getPendingLocalState`. A subsequent live load runs
155
+ // `sharePendingBlobs`, which re-enters `uploadAndAttach` against the real storage to
156
+ // complete the upload. Throwing here would instead delete the cache entry (in
157
+ // `uploadAndAttach`'s catch handler) and lose the blob — defeating the whole point of
158
+ // accruing pending state.
159
+ this.createBlob = readOnly
160
+ ? frozenDocumentStorageServiceHandler
161
+ : async () => this.disposalDeferred.promise;
162
+ }
163
+ dispose() {
164
+ if (this._disposed) {
165
+ return;
166
+ }
167
+ this._disposed = true;
168
+ // Don't propagate any caller-supplied error here. `IDocumentService.dispose` already
169
+ // logs the Container's error path; the createBlob deferred's consumer (BlobManager)
170
+ // only needs the "this storage is going away" signal — not a chain of error causes.
171
+ this.disposalDeferred.reject(new Error("FrozenDocumentStorageService is disposed"));
76
172
  }
77
173
  }
78
174
  const frozenDocumentDeltaStorageService = {
@@ -90,54 +186,71 @@ const clientFrozenDeltaStream = {
90
186
  scopes: [],
91
187
  };
92
188
  const clientIdFrozenDeltaStream = "storage-only client";
189
+ // Cast rationale: ITokenClaims requires tenantId/documentId/user/iat/exp/ver, but a frozen
190
+ // delta stream has no tenant or session to draw real values from — it's a synthetic
191
+ // in-process connection that never reaches a service. Inventing sentinel values would imply
192
+ // quorum membership we cannot honor; only `scopes` actually drives behavior here (DocRead vs
193
+ // DocWrite gates readOnlyInfo). The cast is the honest representation of "this connection
194
+ // has no claims worth populating."
195
+ /* eslint-disable @typescript-eslint/consistent-type-assertions */
196
+ const readOnlyClaims = { scopes: [internal_2.ScopeType.DocRead] };
197
+ const writableClaims = {
198
+ scopes: [internal_2.ScopeType.DocRead, internal_2.ScopeType.DocWrite],
199
+ };
200
+ /* eslint-enable @typescript-eslint/consistent-type-assertions */
93
201
  /**
94
- * Implementation of IDocumentDeltaConnection that does not support submitting
95
- * or receiving ops. Used in storage-only mode and in frozen loads.
202
+ * Inert `IDocumentDeltaConnection` for frozen container loads. Has no server upstream:
203
+ * op and signal streams are empty, and `initialClients` contains only its own synthetic
204
+ * read-only client — which lets the connection state handler observe "self" in the audience
205
+ * and transition the container to Connected without waiting for a real join op or signal.
206
+ *
207
+ * Two concrete variants share this base — see their JSDoc for variant-specific details:
208
+ *
209
+ * - {@link FrozenDeltaStream} — read-only.
210
+ * - {@link WritableFrozenDeltaStream} — writable.
211
+ *
212
+ * Both variants nack any incoming `submit`: this connection has no upstream and
213
+ * `ConnectionManager.sendMessages` recognizes `WritableFrozenDeltaStream` and drops messages
214
+ * before they reach `submit`, so under normal flow it should never fire. A nack reaching the
215
+ * connectionManager surfaces the misuse — and may close the container — which is the right
216
+ * defensive signal that something has bypassed the expected flow.
217
+ *
218
+ * `submitSignal` is a silent no-op for both variants. Signals are ephemeral and best-effort —
219
+ * runtime/presence subsystems may submit them at any point in the writable-frozen lifetime, and
220
+ * dropping them is the correct behavior here (we have no upstream). Closing the container or
221
+ * triggering a reconnect on a stray signal would be strictly worse than dropping it.
96
222
  */
97
- class FrozenDeltaStream extends client_utils_1.TypedEventEmitter {
98
- /**
99
- * Connection which is not connected to socket.
100
- * @param storageOnlyReason - Reason on why the connection to delta stream is not allowed.
101
- * @param readonlyConnectionReason - reason/error if any which lead to using FrozenDeltaStream.
102
- */
103
- constructor(storageOnlyReason, readonlyConnectionReason) {
223
+ class FrozenDeltaStreamBase extends client_utils_1.TypedEventEmitter {
224
+ constructor(clientId, claims) {
104
225
  super();
105
- this.storageOnlyReason = storageOnlyReason;
106
- this.readonlyConnectionReason = readonlyConnectionReason;
107
- this.clientId = clientIdFrozenDeltaStream;
108
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
109
- this.claims = {
110
- scopes: [internal_2.ScopeType.DocRead],
111
- };
112
226
  this.mode = "read";
113
227
  this.existing = true;
114
228
  this.maxMessageSize = 0;
115
229
  this.version = "";
116
230
  this.initialMessages = [];
117
231
  this.initialSignals = [];
118
- this.initialClients = [
119
- { client: clientFrozenDeltaStream, clientId: clientIdFrozenDeltaStream },
120
- ];
121
232
  this.serviceConfiguration = {
122
233
  maxMessageSize: 0,
123
234
  blockSize: 0,
124
235
  };
125
236
  this.checkpointSequenceNumber = undefined;
126
237
  this._disposed = false;
238
+ this.clientId = clientId;
239
+ this.claims = claims;
240
+ // initialClients mirrors clientId so the audience handler observes "self" and
241
+ // transitions the container to Connected without waiting for a real join op or signal.
242
+ this.initialClients = [{ client: clientFrozenDeltaStream, clientId }];
127
243
  }
128
244
  submit(messages) {
129
- this.emit("nack", this.clientId, messages.map((operation) => {
130
- return {
131
- operation,
132
- content: { message: "Cannot submit with storage-only connection", code: 403 },
133
- };
134
- }));
135
- }
136
- submitSignal(message) {
137
- this.emit("nack", this.clientId, {
138
- operation: message,
139
- content: { message: "Cannot submit signal with storage-only connection", code: 403 },
140
- });
245
+ // Defensive nack: nothing should send on a frozen delta stream. If this fires, an
246
+ // invariant in connectionManager has changed and we want it to surface loudly.
247
+ this.emit("nack", this.clientId, messages.map((operation) => ({
248
+ operation,
249
+ content: { message: "Cannot submit on a frozen delta stream", code: 403 },
250
+ })));
251
+ }
252
+ submitSignal(_message) {
253
+ // Intentional no-op. See class JSDoc for rationale.
141
254
  }
142
255
  get disposed() {
143
256
  return this._disposed;
@@ -146,9 +259,74 @@ class FrozenDeltaStream extends client_utils_1.TypedEventEmitter {
146
259
  this._disposed = true;
147
260
  }
148
261
  }
262
+ /**
263
+ * Read-only variant of {@link FrozenDeltaStreamBase}. Claims show only `DocRead`. Used by
264
+ * storage-only loads (where `connectionManager` synthesizes one directly via
265
+ * `policies.storageOnly`) and by the forbidden / out-of-storage fallback paths.
266
+ * {@link isFrozenDeltaStreamConnection} matches this variant and drives the read-only forcing
267
+ * in `ConnectionManager.readOnlyInfo`. Uses the historical `"storage-only client"` constant
268
+ * `clientId`, preserving existing behavior for any consumer that keys off it.
269
+ *
270
+ * `storageOnlyReason` and `readonlyConnectionReason` are surfaced through `IContainer.readOnlyInfo`
271
+ * for diagnostics on the fallback paths (`isDeltaStreamConnectionForbiddenError`,
272
+ * `outOfStorageError`).
273
+ */
274
+ class FrozenDeltaStream extends FrozenDeltaStreamBase {
275
+ constructor(options) {
276
+ // Constant clientId: preserves the pre-PR `"storage-only client"` identity for any
277
+ // consumer that keys off it. The 0x173 replay-assert risk that motivates per-instance
278
+ // clientIds applies only to the writable variant, where the runtime accumulates dirty
279
+ // pending ops across reconnects; the read-only variant does not.
280
+ super(clientIdFrozenDeltaStream, readOnlyClaims);
281
+ this.storageOnlyReason = options?.storageOnlyReason;
282
+ this.readonlyConnectionReason = options?.readonlyConnectionReason;
283
+ }
284
+ }
149
285
  exports.FrozenDeltaStream = FrozenDeltaStream;
286
+ /**
287
+ * Variant of {@link FrozenDeltaStreamBase} that appears to support writing but remains
288
+ * "frozen" — no messages are actually sent or received. The stream itself does not enforce
289
+ * the no-send guarantee; that lives in `ConnectionManager.sendMessages`, which recognizes
290
+ * any `WritableFrozenDeltaStream` (via {@link isWritableFrozenDeltaStreamConnection}) and
291
+ * short-circuits before its read-mode upgrade branch. Submitted ops are dropped at the
292
+ * connection-manager layer, so the container stays `Connected` and the runtime accumulates
293
+ * them in `pendingStateManager`.
294
+ *
295
+ * "Appears writable" mechanics: claims include `DocWrite` so the container surfaces as
296
+ * writable; not matched by {@link isFrozenDeltaStreamConnection}, so `readOnlyInfo` reports
297
+ * `readonly: false`. Connection mode stays `"read"` (advertising `"write"` would imply quorum
298
+ * membership we cannot honor).
299
+ *
300
+ * Each instance mints a fresh `frozen-delta-stream/<uuid>` `clientId` to avoid
301
+ * `pendingStateManager` `0x173` (`replayPendingStates called twice for same clientId!`) on
302
+ * reconnect with dirty pending ops. Sibling (not subclass) of `FrozenDeltaStream` so
303
+ * `instanceof` cleanly distinguishes the two for `ConnectionManager`'s short-circuits.
304
+ */
305
+ class WritableFrozenDeltaStream extends FrozenDeltaStreamBase {
306
+ constructor() {
307
+ super(`frozen-delta-stream/${(0, uuid_1.v4)()}`, writableClaims);
308
+ }
309
+ }
310
+ exports.WritableFrozenDeltaStream = WritableFrozenDeltaStream;
311
+ /**
312
+ * Recognizes the read-only variant of {@link FrozenDeltaStreamBase}. Drives the storage-only
313
+ * forcing in `ConnectionManager.readOnlyInfo`: only the read-only variant should make the
314
+ * container surface as read-only. {@link WritableFrozenDeltaStream} is a sibling class, not
315
+ * a subclass, so `instanceof FrozenDeltaStream` already excludes it.
316
+ */
150
317
  function isFrozenDeltaStreamConnection(connection) {
151
318
  return connection instanceof FrozenDeltaStream;
152
319
  }
153
320
  exports.isFrozenDeltaStreamConnection = isFrozenDeltaStreamConnection;
321
+ /**
322
+ * Recognizes the writable variant of {@link FrozenDeltaStreamBase}. Drives the
323
+ * `ConnectionManager.sendMessages` short-circuit: writable-frozen submits must be dropped at
324
+ * the network layer instead of triggering a read→write reconnect. Sibling (not subclass) of
325
+ * {@link FrozenDeltaStream}, so `instanceof WritableFrozenDeltaStream` excludes the read-only
326
+ * variant.
327
+ */
328
+ function isWritableFrozenDeltaStreamConnection(connection) {
329
+ return connection instanceof WritableFrozenDeltaStream;
330
+ }
331
+ exports.isWritableFrozenDeltaStreamConnection = isWritableFrozenDeltaStreamConnection;
154
332
  //# sourceMappingURL=frozenServices.js.map