@fluidframework/container-loader 2.93.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 (118) hide show
  1. package/CHANGELOG.md +12 -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 +2 -1
  8. package/dist/connectionManager.d.ts.map +1 -1
  9. package/dist/connectionManager.js +39 -8
  10. package/dist/connectionManager.js.map +1 -1
  11. package/dist/connectionStateHandler.d.ts.map +1 -1
  12. package/dist/connectionStateHandler.js +3 -1
  13. package/dist/connectionStateHandler.js.map +1 -1
  14. package/dist/container.d.ts.map +1 -1
  15. package/dist/container.js +13 -4
  16. package/dist/container.js.map +1 -1
  17. package/dist/containerStorageAdapter.d.ts +20 -2
  18. package/dist/containerStorageAdapter.d.ts.map +1 -1
  19. package/dist/containerStorageAdapter.js +2 -2
  20. package/dist/containerStorageAdapter.js.map +1 -1
  21. package/dist/createAndLoadContainerUtils.d.ts +95 -0
  22. package/dist/createAndLoadContainerUtils.d.ts.map +1 -1
  23. package/dist/createAndLoadContainerUtils.js +137 -11
  24. package/dist/createAndLoadContainerUtils.js.map +1 -1
  25. package/dist/frozenServices.d.ts +113 -30
  26. package/dist/frozenServices.d.ts.map +1 -1
  27. package/dist/frozenServices.js +236 -58
  28. package/dist/frozenServices.js.map +1 -1
  29. package/dist/index.d.ts +2 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +5 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/legacyAlpha.d.ts +2 -0
  34. package/dist/loader.d.ts +1 -1
  35. package/dist/loader.d.ts.map +1 -1
  36. package/dist/loader.js +1 -1
  37. package/dist/loader.js.map +1 -1
  38. package/dist/loaderLayerCompatState.d.ts +1 -1
  39. package/dist/packageVersion.d.ts +1 -1
  40. package/dist/packageVersion.d.ts.map +1 -1
  41. package/dist/packageVersion.js +1 -1
  42. package/dist/packageVersion.js.map +1 -1
  43. package/dist/pendingLocalStateStore.d.ts.map +1 -1
  44. package/dist/pendingLocalStateStore.js +9 -3
  45. package/dist/pendingLocalStateStore.js.map +1 -1
  46. package/dist/retriableDocumentStorageService.d.ts +2 -1
  47. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  48. package/dist/retriableDocumentStorageService.js +3 -2
  49. package/dist/retriableDocumentStorageService.js.map +1 -1
  50. package/dist/serializedStateManager.d.ts +16 -1
  51. package/dist/serializedStateManager.d.ts.map +1 -1
  52. package/dist/serializedStateManager.js +11 -1
  53. package/dist/serializedStateManager.js.map +1 -1
  54. package/lib/captureReferencedContents.d.ts +154 -0
  55. package/lib/captureReferencedContents.d.ts.map +1 -0
  56. package/lib/captureReferencedContents.js +338 -0
  57. package/lib/captureReferencedContents.js.map +1 -0
  58. package/lib/connectionManager.d.ts +2 -1
  59. package/lib/connectionManager.d.ts.map +1 -1
  60. package/lib/connectionManager.js +40 -9
  61. package/lib/connectionManager.js.map +1 -1
  62. package/lib/connectionStateHandler.d.ts.map +1 -1
  63. package/lib/connectionStateHandler.js +3 -1
  64. package/lib/connectionStateHandler.js.map +1 -1
  65. package/lib/container.d.ts.map +1 -1
  66. package/lib/container.js +14 -5
  67. package/lib/container.js.map +1 -1
  68. package/lib/containerStorageAdapter.d.ts +20 -2
  69. package/lib/containerStorageAdapter.d.ts.map +1 -1
  70. package/lib/containerStorageAdapter.js +2 -2
  71. package/lib/containerStorageAdapter.js.map +1 -1
  72. package/lib/createAndLoadContainerUtils.d.ts +95 -0
  73. package/lib/createAndLoadContainerUtils.d.ts.map +1 -1
  74. package/lib/createAndLoadContainerUtils.js +128 -3
  75. package/lib/createAndLoadContainerUtils.js.map +1 -1
  76. package/lib/frozenServices.d.ts +113 -30
  77. package/lib/frozenServices.d.ts.map +1 -1
  78. package/lib/frozenServices.js +233 -57
  79. package/lib/frozenServices.js.map +1 -1
  80. package/lib/index.d.ts +2 -1
  81. package/lib/index.d.ts.map +1 -1
  82. package/lib/index.js +2 -1
  83. package/lib/index.js.map +1 -1
  84. package/lib/legacyAlpha.d.ts +2 -0
  85. package/lib/loader.d.ts +1 -1
  86. package/lib/loader.d.ts.map +1 -1
  87. package/lib/loader.js +2 -2
  88. package/lib/loader.js.map +1 -1
  89. package/lib/loaderLayerCompatState.d.ts +1 -1
  90. package/lib/packageVersion.d.ts +1 -1
  91. package/lib/packageVersion.d.ts.map +1 -1
  92. package/lib/packageVersion.js +1 -1
  93. package/lib/packageVersion.js.map +1 -1
  94. package/lib/pendingLocalStateStore.d.ts.map +1 -1
  95. package/lib/pendingLocalStateStore.js +9 -3
  96. package/lib/pendingLocalStateStore.js.map +1 -1
  97. package/lib/retriableDocumentStorageService.d.ts +2 -1
  98. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  99. package/lib/retriableDocumentStorageService.js +3 -2
  100. package/lib/retriableDocumentStorageService.js.map +1 -1
  101. package/lib/serializedStateManager.d.ts +16 -1
  102. package/lib/serializedStateManager.d.ts.map +1 -1
  103. package/lib/serializedStateManager.js +11 -1
  104. package/lib/serializedStateManager.js.map +1 -1
  105. package/package.json +13 -13
  106. package/src/captureReferencedContents.ts +446 -0
  107. package/src/connectionManager.ts +47 -8
  108. package/src/connectionStateHandler.ts +14 -9
  109. package/src/container.ts +18 -4
  110. package/src/containerStorageAdapter.ts +22 -2
  111. package/src/createAndLoadContainerUtils.ts +229 -2
  112. package/src/frozenServices.ts +285 -64
  113. package/src/index.ts +7 -0
  114. package/src/loader.ts +4 -2
  115. package/src/packageVersion.ts +1 -1
  116. package/src/pendingLocalStateStore.ts +8 -1
  117. package/src/retriableDocumentStorageService.ts +11 -4
  118. 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"}
@@ -5,69 +5,165 @@
5
5
  import { TypedEventEmitter } from "@fluid-internal/client-utils";
6
6
  import { isPromiseLike } from "@fluidframework/core-utils/internal";
7
7
  import { ScopeType, } from "@fluidframework/driver-definitions/internal";
8
+ import { v4 as uuid } from "uuid";
8
9
  /**
9
- * Creation of a FrozenDocumentServiceFactory which wraps an existing
10
- * 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.
11
14
  *
12
- * @param documentServiceFactory - The underlying DocumentServiceFactory to wrap.
13
- * @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.
14
29
  * @legacy @alpha
15
30
  */
16
- export function createFrozenDocumentServiceFactory(factory) {
17
- // Sync path
18
- return factory instanceof FrozenDocumentServiceFactory
19
- ? factory
20
- : new FrozenDocumentServiceFactory(factory);
31
+ export function createFrozenDocumentServiceFactory(factory, readOnly = true) {
32
+ if (factory instanceof FrozenDocumentServiceFactory) {
33
+ // Already wrapped. Reuse if readOnly matches; otherwise unwrap and rewrap so the caller's
34
+ // most recent readOnly intent wins (silently honoring caller intent rather than dropping
35
+ // the new argument).
36
+ return factory.readOnly === readOnly
37
+ ? factory
38
+ : new FrozenDocumentServiceFactory(readOnly, factory.inner);
39
+ }
40
+ return new FrozenDocumentServiceFactory(readOnly, factory);
21
41
  }
22
42
  export class FrozenDocumentServiceFactory {
23
- constructor(documentServiceFactory) {
24
- this.documentServiceFactory = documentServiceFactory;
43
+ constructor(readOnly, inner) {
44
+ this.readOnly = readOnly;
45
+ this.inner = inner;
25
46
  }
26
47
  async createDocumentService(resolvedUrl) {
27
- let factory = this.documentServiceFactory;
28
- if (isPromiseLike(factory)) {
29
- factory = await this.documentServiceFactory;
30
- }
31
- return new FrozenDocumentService(resolvedUrl, await factory?.createDocumentService(resolvedUrl));
48
+ const factory = isPromiseLike(this.inner) ? await this.inner : this.inner;
49
+ return new FrozenDocumentService(resolvedUrl, this.readOnly, await factory?.createDocumentService(resolvedUrl));
32
50
  }
33
51
  async createContainer() {
34
52
  throw new Error("The FrozenDocumentServiceFactory cannot be used to create containers.");
35
53
  }
36
54
  }
37
55
  class FrozenDocumentService extends TypedEventEmitter {
38
- constructor(resolvedUrl, documentService) {
56
+ constructor(resolvedUrl, readOnly, documentService) {
39
57
  super();
40
58
  this.resolvedUrl = resolvedUrl;
59
+ this.readOnly = readOnly;
41
60
  this.documentService = documentService;
42
- this.policies = {
43
- storageOnly: true,
44
- };
61
+ // Tracks every storage instance handed out by `connectToStorage` so `dispose()` can cascade
62
+ // disposal to each one (rejecting their hanging `createBlob` deferreds). A Set rather than
63
+ // a single field because `IDocumentService.connectToStorage` is a public API that can be
64
+ // called more than once — we cannot assume the Container holds a single instance.
65
+ this.storageServices = new Set();
66
+ // When readOnly, advertise the storageOnly policy. The connectionManager short-circuits
67
+ // on it: it synthesizes a FrozenDeltaStream itself and never calls
68
+ // connectToDeltaStream, and the readOnlyInfo getter forces the container to read-only
69
+ // because the live connection is a FrozenDeltaStream.
70
+ //
71
+ // Audit (2026-05-05): the only consumer of `policies.storageOnly` as a frozen-container
72
+ // signal is `ConnectionManager` (synthesizing a `FrozenDeltaStream` when set). All other
73
+ // matches in the loader/runtime/driver layers are either drivers reading their own
74
+ // policies (e.g. local-driver) or `IReadOnlyInfo.storageOnly`, which is derived from the
75
+ // live connection — not the policy. So the writable-frozen container is intentionally
76
+ // indistinguishable from a normal container at the policies layer; downstream behavior
77
+ // flows through the live `WritableFrozenDeltaStream` instead.
78
+ this.policies = readOnly ? { storageOnly: true } : {};
45
79
  }
46
80
  async connectToStorage() {
47
- return new FrozenDocumentStorageService(await this.documentService?.connectToStorage());
81
+ const storage = new FrozenDocumentStorageService(this.readOnly, await this.documentService?.connectToStorage());
82
+ this.storageServices.add(storage);
83
+ return storage;
48
84
  }
49
85
  async connectToDeltaStorage() {
50
86
  return frozenDocumentDeltaStorageService;
51
87
  }
52
- async connectToDeltaStream(client) {
53
- return new FrozenDeltaStream();
88
+ async connectToDeltaStream(_client) {
89
+ if (this.readOnly) {
90
+ // connectionManager short-circuits via policies.storageOnly before reaching here
91
+ // in the read-only path; reaching this branch indicates a non-connectionManager
92
+ // consumer or a regression of the short-circuit. Throw to surface the misuse
93
+ // rather than silently produce a working stream.
94
+ throw new Error("FrozenDocumentService is read-only; connectToDeltaStream should not be called (connectionManager short-circuits via policies.storageOnly)");
95
+ }
96
+ // Writable path: hand out a fresh WritableFrozenDeltaStream regardless of client.mode
97
+ // or whether this is the initial connect or a reconnect. The stream's own mode is
98
+ // "read" (advertising "write" would imply quorum membership we cannot honor), and
99
+ // `ConnectionManager.sendMessages` short-circuits on WritableFrozenDeltaStream so
100
+ // outbound writes never reach a real network. The per-instance clientId minted in
101
+ // FrozenDeltaStreamBase prevents pendingStateManager 0x173 on replay across reconnects.
102
+ return new WritableFrozenDeltaStream();
103
+ }
104
+ dispose(error) {
105
+ // Cascade disposal to each storage instance so any hanging `createBlob` promises (the
106
+ // writable-frozen pending-blob mechanism) reject and the BlobManager can release its
107
+ // references. Without this, hung promises remain held by BlobManager closures for the
108
+ // lifetime of the process.
109
+ for (const storage of this.storageServices) {
110
+ storage.dispose();
111
+ }
112
+ this.storageServices.clear();
113
+ // Forward disposal to the wrapped service. We own its lifetime (it was created for us
114
+ // by the wrapping factory and is never exposed to callers), so the contract from
115
+ // `IDocumentService.dispose` ("called by storage consumer when done with storage")
116
+ // applies here.
117
+ this.documentService?.dispose(error);
54
118
  }
55
- dispose() { }
56
119
  }
57
120
  const frozenDocumentStorageServiceHandler = () => {
58
121
  throw new Error("Operations are not supported on the FrozenDocumentStorageService.");
59
122
  };
60
123
  class FrozenDocumentStorageService {
61
- constructor(documentStorageService) {
124
+ get disposed() {
125
+ return this._disposed;
126
+ }
127
+ constructor(readOnly, documentStorageService) {
62
128
  this.documentStorageService = documentStorageService;
129
+ this._disposed = false;
63
130
  this.getSnapshotTree = frozenDocumentStorageServiceHandler;
64
131
  this.getSnapshot = frozenDocumentStorageServiceHandler;
65
132
  this.getVersions = frozenDocumentStorageServiceHandler;
66
- this.createBlob = frozenDocumentStorageServiceHandler;
67
133
  this.readBlob = this.documentStorageService?.readBlob.bind(this.documentStorageService) ??
68
134
  frozenDocumentStorageServiceHandler;
69
135
  this.uploadSummaryWithContext = frozenDocumentStorageServiceHandler;
70
136
  this.downloadSummary = frozenDocumentStorageServiceHandler;
137
+ let rejectFn;
138
+ const promise = new Promise((_, reject) => {
139
+ rejectFn = reject;
140
+ });
141
+ // Attach a no-op catch so node doesn't log an unhandled-rejection warning when
142
+ // dispose runs before any caller has awaited the promise. Callers awaiting the
143
+ // original promise still observe the rejection.
144
+ promise.catch(() => { });
145
+ this.disposalDeferred = { promise, reject: rejectFn };
146
+ // In the writable-frozen path, `createBlob` returns a never-resolving promise instead
147
+ // of throwing. This keeps the BlobManager's `localBlobCache` entry in the `uploading`
148
+ // state: `getPendingBlobs` downgrades `uploading` blobs to `localOnly` in pending
149
+ // state, so the blob survives `getPendingLocalState`. A subsequent live load runs
150
+ // `sharePendingBlobs`, which re-enters `uploadAndAttach` against the real storage to
151
+ // complete the upload. Throwing here would instead delete the cache entry (in
152
+ // `uploadAndAttach`'s catch handler) and lose the blob — defeating the whole point of
153
+ // accruing pending state.
154
+ this.createBlob = readOnly
155
+ ? frozenDocumentStorageServiceHandler
156
+ : async () => this.disposalDeferred.promise;
157
+ }
158
+ dispose() {
159
+ if (this._disposed) {
160
+ return;
161
+ }
162
+ this._disposed = true;
163
+ // Don't propagate any caller-supplied error here. `IDocumentService.dispose` already
164
+ // logs the Container's error path; the createBlob deferred's consumer (BlobManager)
165
+ // only needs the "this storage is going away" signal — not a chain of error causes.
166
+ this.disposalDeferred.reject(new Error("FrozenDocumentStorageService is disposed"));
71
167
  }
72
168
  }
73
169
  const frozenDocumentDeltaStorageService = {
@@ -85,54 +181,71 @@ const clientFrozenDeltaStream = {
85
181
  scopes: [],
86
182
  };
87
183
  const clientIdFrozenDeltaStream = "storage-only client";
184
+ // Cast rationale: ITokenClaims requires tenantId/documentId/user/iat/exp/ver, but a frozen
185
+ // delta stream has no tenant or session to draw real values from — it's a synthetic
186
+ // in-process connection that never reaches a service. Inventing sentinel values would imply
187
+ // quorum membership we cannot honor; only `scopes` actually drives behavior here (DocRead vs
188
+ // DocWrite gates readOnlyInfo). The cast is the honest representation of "this connection
189
+ // has no claims worth populating."
190
+ /* eslint-disable @typescript-eslint/consistent-type-assertions */
191
+ const readOnlyClaims = { scopes: [ScopeType.DocRead] };
192
+ const writableClaims = {
193
+ scopes: [ScopeType.DocRead, ScopeType.DocWrite],
194
+ };
195
+ /* eslint-enable @typescript-eslint/consistent-type-assertions */
88
196
  /**
89
- * Implementation of IDocumentDeltaConnection that does not support submitting
90
- * or receiving ops. Used in storage-only mode and in frozen loads.
197
+ * Inert `IDocumentDeltaConnection` for frozen container loads. Has no server upstream:
198
+ * op and signal streams are empty, and `initialClients` contains only its own synthetic
199
+ * read-only client — which lets the connection state handler observe "self" in the audience
200
+ * and transition the container to Connected without waiting for a real join op or signal.
201
+ *
202
+ * Two concrete variants share this base — see their JSDoc for variant-specific details:
203
+ *
204
+ * - {@link FrozenDeltaStream} — read-only.
205
+ * - {@link WritableFrozenDeltaStream} — writable.
206
+ *
207
+ * Both variants nack any incoming `submit`: this connection has no upstream and
208
+ * `ConnectionManager.sendMessages` recognizes `WritableFrozenDeltaStream` and drops messages
209
+ * before they reach `submit`, so under normal flow it should never fire. A nack reaching the
210
+ * connectionManager surfaces the misuse — and may close the container — which is the right
211
+ * defensive signal that something has bypassed the expected flow.
212
+ *
213
+ * `submitSignal` is a silent no-op for both variants. Signals are ephemeral and best-effort —
214
+ * runtime/presence subsystems may submit them at any point in the writable-frozen lifetime, and
215
+ * dropping them is the correct behavior here (we have no upstream). Closing the container or
216
+ * triggering a reconnect on a stray signal would be strictly worse than dropping it.
91
217
  */
92
- export class FrozenDeltaStream extends TypedEventEmitter {
93
- /**
94
- * Connection which is not connected to socket.
95
- * @param storageOnlyReason - Reason on why the connection to delta stream is not allowed.
96
- * @param readonlyConnectionReason - reason/error if any which lead to using FrozenDeltaStream.
97
- */
98
- constructor(storageOnlyReason, readonlyConnectionReason) {
218
+ class FrozenDeltaStreamBase extends TypedEventEmitter {
219
+ constructor(clientId, claims) {
99
220
  super();
100
- this.storageOnlyReason = storageOnlyReason;
101
- this.readonlyConnectionReason = readonlyConnectionReason;
102
- this.clientId = clientIdFrozenDeltaStream;
103
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
104
- this.claims = {
105
- scopes: [ScopeType.DocRead],
106
- };
107
221
  this.mode = "read";
108
222
  this.existing = true;
109
223
  this.maxMessageSize = 0;
110
224
  this.version = "";
111
225
  this.initialMessages = [];
112
226
  this.initialSignals = [];
113
- this.initialClients = [
114
- { client: clientFrozenDeltaStream, clientId: clientIdFrozenDeltaStream },
115
- ];
116
227
  this.serviceConfiguration = {
117
228
  maxMessageSize: 0,
118
229
  blockSize: 0,
119
230
  };
120
231
  this.checkpointSequenceNumber = undefined;
121
232
  this._disposed = false;
233
+ this.clientId = clientId;
234
+ this.claims = claims;
235
+ // initialClients mirrors clientId so the audience handler observes "self" and
236
+ // transitions the container to Connected without waiting for a real join op or signal.
237
+ this.initialClients = [{ client: clientFrozenDeltaStream, clientId }];
122
238
  }
123
239
  submit(messages) {
124
- this.emit("nack", this.clientId, messages.map((operation) => {
125
- return {
126
- operation,
127
- content: { message: "Cannot submit with storage-only connection", code: 403 },
128
- };
129
- }));
130
- }
131
- submitSignal(message) {
132
- this.emit("nack", this.clientId, {
133
- operation: message,
134
- content: { message: "Cannot submit signal with storage-only connection", code: 403 },
135
- });
240
+ // Defensive nack: nothing should send on a frozen delta stream. If this fires, an
241
+ // invariant in connectionManager has changed and we want it to surface loudly.
242
+ this.emit("nack", this.clientId, messages.map((operation) => ({
243
+ operation,
244
+ content: { message: "Cannot submit on a frozen delta stream", code: 403 },
245
+ })));
246
+ }
247
+ submitSignal(_message) {
248
+ // Intentional no-op. See class JSDoc for rationale.
136
249
  }
137
250
  get disposed() {
138
251
  return this._disposed;
@@ -141,7 +254,70 @@ export class FrozenDeltaStream extends TypedEventEmitter {
141
254
  this._disposed = true;
142
255
  }
143
256
  }
257
+ /**
258
+ * Read-only variant of {@link FrozenDeltaStreamBase}. Claims show only `DocRead`. Used by
259
+ * storage-only loads (where `connectionManager` synthesizes one directly via
260
+ * `policies.storageOnly`) and by the forbidden / out-of-storage fallback paths.
261
+ * {@link isFrozenDeltaStreamConnection} matches this variant and drives the read-only forcing
262
+ * in `ConnectionManager.readOnlyInfo`. Uses the historical `"storage-only client"` constant
263
+ * `clientId`, preserving existing behavior for any consumer that keys off it.
264
+ *
265
+ * `storageOnlyReason` and `readonlyConnectionReason` are surfaced through `IContainer.readOnlyInfo`
266
+ * for diagnostics on the fallback paths (`isDeltaStreamConnectionForbiddenError`,
267
+ * `outOfStorageError`).
268
+ */
269
+ export class FrozenDeltaStream extends FrozenDeltaStreamBase {
270
+ constructor(options) {
271
+ // Constant clientId: preserves the pre-PR `"storage-only client"` identity for any
272
+ // consumer that keys off it. The 0x173 replay-assert risk that motivates per-instance
273
+ // clientIds applies only to the writable variant, where the runtime accumulates dirty
274
+ // pending ops across reconnects; the read-only variant does not.
275
+ super(clientIdFrozenDeltaStream, readOnlyClaims);
276
+ this.storageOnlyReason = options?.storageOnlyReason;
277
+ this.readonlyConnectionReason = options?.readonlyConnectionReason;
278
+ }
279
+ }
280
+ /**
281
+ * Variant of {@link FrozenDeltaStreamBase} that appears to support writing but remains
282
+ * "frozen" — no messages are actually sent or received. The stream itself does not enforce
283
+ * the no-send guarantee; that lives in `ConnectionManager.sendMessages`, which recognizes
284
+ * any `WritableFrozenDeltaStream` (via {@link isWritableFrozenDeltaStreamConnection}) and
285
+ * short-circuits before its read-mode upgrade branch. Submitted ops are dropped at the
286
+ * connection-manager layer, so the container stays `Connected` and the runtime accumulates
287
+ * them in `pendingStateManager`.
288
+ *
289
+ * "Appears writable" mechanics: claims include `DocWrite` so the container surfaces as
290
+ * writable; not matched by {@link isFrozenDeltaStreamConnection}, so `readOnlyInfo` reports
291
+ * `readonly: false`. Connection mode stays `"read"` (advertising `"write"` would imply quorum
292
+ * membership we cannot honor).
293
+ *
294
+ * Each instance mints a fresh `frozen-delta-stream/<uuid>` `clientId` to avoid
295
+ * `pendingStateManager` `0x173` (`replayPendingStates called twice for same clientId!`) on
296
+ * reconnect with dirty pending ops. Sibling (not subclass) of `FrozenDeltaStream` so
297
+ * `instanceof` cleanly distinguishes the two for `ConnectionManager`'s short-circuits.
298
+ */
299
+ export class WritableFrozenDeltaStream extends FrozenDeltaStreamBase {
300
+ constructor() {
301
+ super(`frozen-delta-stream/${uuid()}`, writableClaims);
302
+ }
303
+ }
304
+ /**
305
+ * Recognizes the read-only variant of {@link FrozenDeltaStreamBase}. Drives the storage-only
306
+ * forcing in `ConnectionManager.readOnlyInfo`: only the read-only variant should make the
307
+ * container surface as read-only. {@link WritableFrozenDeltaStream} is a sibling class, not
308
+ * a subclass, so `instanceof FrozenDeltaStream` already excludes it.
309
+ */
144
310
  export function isFrozenDeltaStreamConnection(connection) {
145
311
  return connection instanceof FrozenDeltaStream;
146
312
  }
313
+ /**
314
+ * Recognizes the writable variant of {@link FrozenDeltaStreamBase}. Drives the
315
+ * `ConnectionManager.sendMessages` short-circuit: writable-frozen submits must be dropped at
316
+ * the network layer instead of triggering a read→write reconnect. Sibling (not subclass) of
317
+ * {@link FrozenDeltaStream}, so `instanceof WritableFrozenDeltaStream` excludes the read-only
318
+ * variant.
319
+ */
320
+ export function isWritableFrozenDeltaStreamConnection(connection) {
321
+ return connection instanceof WritableFrozenDeltaStream;
322
+ }
147
323
  //# sourceMappingURL=frozenServices.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"frozenServices.js","sourceRoot":"","sources":["../src/frozenServices.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { TypedEventEmitter } from \"@fluid-internal/client-utils\";\nimport type { IDisposable } from \"@fluidframework/core-interfaces\";\nimport { isPromiseLike } from \"@fluidframework/core-utils/internal\";\nimport {\n\tScopeType,\n\ttype ConnectionMode,\n\ttype IClient,\n\ttype IClientConfiguration,\n\ttype IDocumentDeltaConnection,\n\ttype IDocumentDeltaConnectionEvents,\n\ttype IDocumentDeltaStorageService,\n\ttype IDocumentMessage,\n\ttype IDocumentService,\n\ttype IDocumentServiceEvents,\n\ttype IDocumentServiceFactory,\n\ttype IDocumentServicePolicies,\n\ttype IDocumentStorageService,\n\ttype IResolvedUrl,\n\ttype ISequencedDocumentMessage,\n\ttype ISignalClient,\n\ttype ISignalMessage,\n\ttype ITokenClaims,\n} from \"@fluidframework/driver-definitions/internal\";\n\nimport type { IConnectionStateChangeReason } from \"./contracts.js\";\n\n/**\n * Creation of a FrozenDocumentServiceFactory which wraps an existing\n * DocumentServiceFactory to provide a storage-only document service.\n *\n * @param documentServiceFactory - The underlying DocumentServiceFactory to wrap.\n * @returns A FrozenDocumentServiceFactory\n * @legacy @alpha\n */\nexport function createFrozenDocumentServiceFactory(\n\tfactory?: IDocumentServiceFactory | Promise<IDocumentServiceFactory>,\n): IDocumentServiceFactory {\n\t// Sync path\n\treturn factory instanceof FrozenDocumentServiceFactory\n\t\t? factory\n\t\t: new FrozenDocumentServiceFactory(factory);\n}\n\nexport class FrozenDocumentServiceFactory implements IDocumentServiceFactory {\n\tconstructor(\n\t\tprivate readonly documentServiceFactory?:\n\t\t\t| IDocumentServiceFactory\n\t\t\t| Promise<IDocumentServiceFactory>,\n\t) {}\n\n\tasync createDocumentService(resolvedUrl: IResolvedUrl): Promise<IDocumentService> {\n\t\tlet factory = this.documentServiceFactory;\n\t\tif (isPromiseLike(factory)) {\n\t\t\tfactory = await this.documentServiceFactory;\n\t\t}\n\t\treturn new FrozenDocumentService(\n\t\t\tresolvedUrl,\n\t\t\tawait factory?.createDocumentService(resolvedUrl),\n\t\t);\n\t}\n\tasync createContainer(): Promise<IDocumentService> {\n\t\tthrow new Error(\"The FrozenDocumentServiceFactory cannot be used to create containers.\");\n\t}\n}\n\nclass FrozenDocumentService\n\textends TypedEventEmitter<IDocumentServiceEvents>\n\timplements IDocumentService\n{\n\tconstructor(\n\t\tpublic readonly resolvedUrl: IResolvedUrl,\n\t\tprivate readonly documentService?: IDocumentService,\n\t) {\n\t\tsuper();\n\t}\n\n\tpublic readonly policies: IDocumentServicePolicies = {\n\t\tstorageOnly: true,\n\t};\n\tasync connectToStorage(): Promise<IDocumentStorageService> {\n\t\treturn new FrozenDocumentStorageService(await this.documentService?.connectToStorage());\n\t}\n\tasync connectToDeltaStorage(): Promise<IDocumentDeltaStorageService> {\n\t\treturn frozenDocumentDeltaStorageService;\n\t}\n\tasync connectToDeltaStream(client: IClient): Promise<IDocumentDeltaConnection> {\n\t\treturn new FrozenDeltaStream();\n\t}\n\tdispose(): void {}\n}\n\nconst frozenDocumentStorageServiceHandler = (): never => {\n\tthrow new Error(\"Operations are not supported on the FrozenDocumentStorageService.\");\n};\nclass FrozenDocumentStorageService implements IDocumentStorageService {\n\tconstructor(private readonly documentStorageService?: IDocumentStorageService) {}\n\n\tgetSnapshotTree = frozenDocumentStorageServiceHandler;\n\tgetSnapshot = frozenDocumentStorageServiceHandler;\n\tgetVersions = frozenDocumentStorageServiceHandler;\n\tcreateBlob = frozenDocumentStorageServiceHandler;\n\treadBlob =\n\t\tthis.documentStorageService?.readBlob.bind(this.documentStorageService) ??\n\t\tfrozenDocumentStorageServiceHandler;\n\tuploadSummaryWithContext = frozenDocumentStorageServiceHandler;\n\tdownloadSummary = frozenDocumentStorageServiceHandler;\n}\n\nconst frozenDocumentDeltaStorageService: IDocumentDeltaStorageService = {\n\tfetchMessages: () => ({\n\t\tread: async () => ({\n\t\t\tdone: true,\n\t\t}),\n\t}),\n};\n\nconst clientFrozenDeltaStream: IClient = {\n\tmode: \"read\",\n\tdetails: { capabilities: { interactive: true } },\n\tpermission: [],\n\tuser: { id: \"storage-only client\" }, // we need some \"fake\" ID here.\n\tscopes: [],\n};\nconst clientIdFrozenDeltaStream: string = \"storage-only client\";\n\n/**\n * Implementation of IDocumentDeltaConnection that does not support submitting\n * or receiving ops. Used in storage-only mode and in frozen loads.\n */\nexport class FrozenDeltaStream\n\textends TypedEventEmitter<IDocumentDeltaConnectionEvents>\n\timplements IDocumentDeltaConnection, IDisposable\n{\n\tclientId = clientIdFrozenDeltaStream;\n\t// eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n\tclaims = {\n\t\tscopes: [ScopeType.DocRead],\n\t} as ITokenClaims;\n\tmode: ConnectionMode = \"read\";\n\texisting: boolean = true;\n\tmaxMessageSize: number = 0;\n\tversion: string = \"\";\n\tinitialMessages: ISequencedDocumentMessage[] = [];\n\tinitialSignals: ISignalMessage[] = [];\n\tinitialClients: ISignalClient[] = [\n\t\t{ client: clientFrozenDeltaStream, clientId: clientIdFrozenDeltaStream },\n\t];\n\tserviceConfiguration: IClientConfiguration = {\n\t\tmaxMessageSize: 0,\n\t\tblockSize: 0,\n\t};\n\tcheckpointSequenceNumber?: number | undefined = undefined;\n\t/**\n\t * Connection which is not connected to socket.\n\t * @param storageOnlyReason - Reason on why the connection to delta stream is not allowed.\n\t * @param readonlyConnectionReason - reason/error if any which lead to using FrozenDeltaStream.\n\t */\n\tconstructor(\n\t\tpublic readonly storageOnlyReason?: string,\n\t\tpublic readonly readonlyConnectionReason?: IConnectionStateChangeReason,\n\t) {\n\t\tsuper();\n\t}\n\tsubmit(messages: IDocumentMessage[]): void {\n\t\tthis.emit(\n\t\t\t\"nack\",\n\t\t\tthis.clientId,\n\t\t\tmessages.map((operation) => {\n\t\t\t\treturn {\n\t\t\t\t\toperation,\n\t\t\t\t\tcontent: { message: \"Cannot submit with storage-only connection\", code: 403 },\n\t\t\t\t};\n\t\t\t}),\n\t\t);\n\t}\n\tsubmitSignal(message: unknown): void {\n\t\tthis.emit(\"nack\", this.clientId, {\n\t\t\toperation: message,\n\t\t\tcontent: { message: \"Cannot submit signal with storage-only connection\", code: 403 },\n\t\t});\n\t}\n\n\tprivate _disposed = false;\n\tpublic get disposed(): boolean {\n\t\treturn this._disposed;\n\t}\n\tpublic dispose(): void {\n\t\tthis._disposed = true;\n\t}\n}\nexport function isFrozenDeltaStreamConnection(\n\tconnection: unknown,\n): connection is FrozenDeltaStream {\n\treturn connection instanceof FrozenDeltaStream;\n}\n"]}
1
+ {"version":3,"file":"frozenServices.js","sourceRoot":"","sources":["../src/frozenServices.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { TypedEventEmitter } from \"@fluid-internal/client-utils\";\nimport type { IDisposable } from \"@fluidframework/core-interfaces\";\nimport { isPromiseLike } from \"@fluidframework/core-utils/internal\";\nimport {\n\tScopeType,\n\ttype ConnectionMode,\n\ttype IClient,\n\ttype IClientConfiguration,\n\ttype IDocumentDeltaConnection,\n\ttype IDocumentDeltaConnectionEvents,\n\ttype IDocumentDeltaStorageService,\n\ttype IDocumentMessage,\n\ttype IDocumentService,\n\ttype IDocumentServiceEvents,\n\ttype IDocumentServiceFactory,\n\ttype IDocumentServicePolicies,\n\ttype IDocumentStorageService,\n\ttype IResolvedUrl,\n\ttype ISequencedDocumentMessage,\n\ttype ISignalClient,\n\ttype ISignalMessage,\n\ttype ITokenClaims,\n} from \"@fluidframework/driver-definitions/internal\";\nimport { v4 as uuid } from \"uuid\";\n\nimport type { IConnectionStateChangeReason } from \"./contracts.js\";\n\n/**\n * Creates an `IDocumentServiceFactory` that produces a \"frozen\" document service: one whose\n * delta stream never sends or receives ops, and whose storage service only supports\n * `IDocumentStorageService.readBlob`. Used to load a container from pending local state\n * without re-establishing a live connection.\n *\n * @param factory - The underlying factory to wrap. Its storage backs blob reads; all other\n * storage operations throw. May be omitted when blob fetches are not required.\n * @param readOnly - When `true` (the default), the document service advertises the\n * `IDocumentServicePolicies.storageOnly` policy, which causes the loader to surface the\n * container as read-only (see `IContainer.readOnlyInfo`).\n *\n * When `false`, the container is loaded as writable so the runtime will accept DDS submissions.\n * The connection itself stays `Connected`: `ConnectionManager.sendMessages` recognizes the\n * `WritableFrozenDeltaStream` as the live connection and short-circuits — the message is dropped\n * at the network layer rather than triggering a read→write reconnect. Local DDS state continues\n * to update via optimistic apply, and submitted ops accumulate in the runtime's pending-state\n * manager, which is exactly the state needed to capture pending local state. Use `false` when\n * callers want to accrue and capture pending state without publishing it.\n * @returns A factory that produces frozen document services.\n * @legacy @alpha\n */\nexport function createFrozenDocumentServiceFactory(\n\tfactory?: IDocumentServiceFactory | Promise<IDocumentServiceFactory>,\n\treadOnly: boolean = true,\n): IDocumentServiceFactory {\n\tif (factory instanceof FrozenDocumentServiceFactory) {\n\t\t// Already wrapped. Reuse if readOnly matches; otherwise unwrap and rewrap so the caller's\n\t\t// most recent readOnly intent wins (silently honoring caller intent rather than dropping\n\t\t// the new argument).\n\t\treturn factory.readOnly === readOnly\n\t\t\t? factory\n\t\t\t: new FrozenDocumentServiceFactory(readOnly, factory.inner);\n\t}\n\treturn new FrozenDocumentServiceFactory(readOnly, factory);\n}\n\nexport class FrozenDocumentServiceFactory implements IDocumentServiceFactory {\n\tconstructor(\n\t\tpublic readonly readOnly: boolean,\n\t\tpublic readonly inner?: IDocumentServiceFactory | Promise<IDocumentServiceFactory>,\n\t) {}\n\n\tasync createDocumentService(resolvedUrl: IResolvedUrl): Promise<IDocumentService> {\n\t\tconst factory = isPromiseLike(this.inner) ? await this.inner : this.inner;\n\t\treturn new FrozenDocumentService(\n\t\t\tresolvedUrl,\n\t\t\tthis.readOnly,\n\t\t\tawait factory?.createDocumentService(resolvedUrl),\n\t\t);\n\t}\n\tasync createContainer(): Promise<IDocumentService> {\n\t\tthrow new Error(\"The FrozenDocumentServiceFactory cannot be used to create containers.\");\n\t}\n}\n\nclass FrozenDocumentService\n\textends TypedEventEmitter<IDocumentServiceEvents>\n\timplements IDocumentService\n{\n\t// Tracks every storage instance handed out by `connectToStorage` so `dispose()` can cascade\n\t// disposal to each one (rejecting their hanging `createBlob` deferreds). A Set rather than\n\t// a single field because `IDocumentService.connectToStorage` is a public API that can be\n\t// called more than once — we cannot assume the Container holds a single instance.\n\tprivate readonly storageServices = new Set<FrozenDocumentStorageService>();\n\n\tconstructor(\n\t\tpublic readonly resolvedUrl: IResolvedUrl,\n\t\tprivate readonly readOnly: boolean,\n\t\tprivate readonly documentService?: IDocumentService,\n\t) {\n\t\tsuper();\n\t\t// When readOnly, advertise the storageOnly policy. The connectionManager short-circuits\n\t\t// on it: it synthesizes a FrozenDeltaStream itself and never calls\n\t\t// connectToDeltaStream, and the readOnlyInfo getter forces the container to read-only\n\t\t// because the live connection is a FrozenDeltaStream.\n\t\t//\n\t\t// Audit (2026-05-05): the only consumer of `policies.storageOnly` as a frozen-container\n\t\t// signal is `ConnectionManager` (synthesizing a `FrozenDeltaStream` when set). All other\n\t\t// matches in the loader/runtime/driver layers are either drivers reading their own\n\t\t// policies (e.g. local-driver) or `IReadOnlyInfo.storageOnly`, which is derived from the\n\t\t// live connection — not the policy. So the writable-frozen container is intentionally\n\t\t// indistinguishable from a normal container at the policies layer; downstream behavior\n\t\t// flows through the live `WritableFrozenDeltaStream` instead.\n\t\tthis.policies = readOnly ? { storageOnly: true } : {};\n\t}\n\n\tpublic readonly policies: IDocumentServicePolicies;\n\tasync connectToStorage(): Promise<IDocumentStorageService> {\n\t\tconst storage = new FrozenDocumentStorageService(\n\t\t\tthis.readOnly,\n\t\t\tawait this.documentService?.connectToStorage(),\n\t\t);\n\t\tthis.storageServices.add(storage);\n\t\treturn storage;\n\t}\n\tasync connectToDeltaStorage(): Promise<IDocumentDeltaStorageService> {\n\t\treturn frozenDocumentDeltaStorageService;\n\t}\n\tasync connectToDeltaStream(_client: IClient): Promise<IDocumentDeltaConnection> {\n\t\tif (this.readOnly) {\n\t\t\t// connectionManager short-circuits via policies.storageOnly before reaching here\n\t\t\t// in the read-only path; reaching this branch indicates a non-connectionManager\n\t\t\t// consumer or a regression of the short-circuit. Throw to surface the misuse\n\t\t\t// rather than silently produce a working stream.\n\t\t\tthrow new Error(\n\t\t\t\t\"FrozenDocumentService is read-only; connectToDeltaStream should not be called (connectionManager short-circuits via policies.storageOnly)\",\n\t\t\t);\n\t\t}\n\t\t// Writable path: hand out a fresh WritableFrozenDeltaStream regardless of client.mode\n\t\t// or whether this is the initial connect or a reconnect. The stream's own mode is\n\t\t// \"read\" (advertising \"write\" would imply quorum membership we cannot honor), and\n\t\t// `ConnectionManager.sendMessages` short-circuits on WritableFrozenDeltaStream so\n\t\t// outbound writes never reach a real network. The per-instance clientId minted in\n\t\t// FrozenDeltaStreamBase prevents pendingStateManager 0x173 on replay across reconnects.\n\t\treturn new WritableFrozenDeltaStream();\n\t}\n\tdispose(error?: unknown): void {\n\t\t// Cascade disposal to each storage instance so any hanging `createBlob` promises (the\n\t\t// writable-frozen pending-blob mechanism) reject and the BlobManager can release its\n\t\t// references. Without this, hung promises remain held by BlobManager closures for the\n\t\t// lifetime of the process.\n\t\tfor (const storage of this.storageServices) {\n\t\t\tstorage.dispose();\n\t\t}\n\t\tthis.storageServices.clear();\n\t\t// Forward disposal to the wrapped service. We own its lifetime (it was created for us\n\t\t// by the wrapping factory and is never exposed to callers), so the contract from\n\t\t// `IDocumentService.dispose` (\"called by storage consumer when done with storage\")\n\t\t// applies here.\n\t\tthis.documentService?.dispose(error);\n\t}\n}\n\nconst frozenDocumentStorageServiceHandler = (): never => {\n\tthrow new Error(\"Operations are not supported on the FrozenDocumentStorageService.\");\n};\n\nclass FrozenDocumentStorageService implements IDocumentStorageService, IDisposable {\n\t// Single deferred shared by every in-flight `createBlob` call. The writable-frozen\n\t// `createBlob` returns this promise so the BlobManager keeps the blob in `uploading`\n\t// state (see comment in the constructor). Rejecting the deferred on disposal fans the\n\t// rejection out to every awaiter at once — and to any future `createBlob` calls too,\n\t// since they receive the already-rejected promise.\n\tprivate readonly disposalDeferred: {\n\t\treadonly promise: Promise<never>;\n\t\treadonly reject: (error: Error) => void;\n\t};\n\n\tprivate _disposed = false;\n\tpublic get disposed(): boolean {\n\t\treturn this._disposed;\n\t}\n\n\tconstructor(\n\t\treadOnly: boolean,\n\t\tprivate readonly documentStorageService?: IDocumentStorageService,\n\t) {\n\t\tlet rejectFn!: (error: Error) => void;\n\t\tconst promise = new Promise<never>((_, reject) => {\n\t\t\trejectFn = reject;\n\t\t});\n\t\t// Attach a no-op catch so node doesn't log an unhandled-rejection warning when\n\t\t// dispose runs before any caller has awaited the promise. Callers awaiting the\n\t\t// original promise still observe the rejection.\n\t\tpromise.catch(() => {});\n\t\tthis.disposalDeferred = { promise, reject: rejectFn };\n\n\t\t// In the writable-frozen path, `createBlob` returns a never-resolving promise instead\n\t\t// of throwing. This keeps the BlobManager's `localBlobCache` entry in the `uploading`\n\t\t// state: `getPendingBlobs` downgrades `uploading` blobs to `localOnly` in pending\n\t\t// state, so the blob survives `getPendingLocalState`. A subsequent live load runs\n\t\t// `sharePendingBlobs`, which re-enters `uploadAndAttach` against the real storage to\n\t\t// complete the upload. Throwing here would instead delete the cache entry (in\n\t\t// `uploadAndAttach`'s catch handler) and lose the blob — defeating the whole point of\n\t\t// accruing pending state.\n\t\tthis.createBlob = readOnly\n\t\t\t? frozenDocumentStorageServiceHandler\n\t\t\t: async () => this.disposalDeferred.promise;\n\t}\n\n\tgetSnapshotTree = frozenDocumentStorageServiceHandler;\n\tgetSnapshot = frozenDocumentStorageServiceHandler;\n\tgetVersions = frozenDocumentStorageServiceHandler;\n\tcreateBlob: IDocumentStorageService[\"createBlob\"];\n\treadBlob =\n\t\tthis.documentStorageService?.readBlob.bind(this.documentStorageService) ??\n\t\tfrozenDocumentStorageServiceHandler;\n\tuploadSummaryWithContext = frozenDocumentStorageServiceHandler;\n\tdownloadSummary = frozenDocumentStorageServiceHandler;\n\n\tpublic dispose(): void {\n\t\tif (this._disposed) {\n\t\t\treturn;\n\t\t}\n\t\tthis._disposed = true;\n\t\t// Don't propagate any caller-supplied error here. `IDocumentService.dispose` already\n\t\t// logs the Container's error path; the createBlob deferred's consumer (BlobManager)\n\t\t// only needs the \"this storage is going away\" signal — not a chain of error causes.\n\t\tthis.disposalDeferred.reject(new Error(\"FrozenDocumentStorageService is disposed\"));\n\t}\n}\n\nconst frozenDocumentDeltaStorageService: IDocumentDeltaStorageService = {\n\tfetchMessages: () => ({\n\t\tread: async () => ({\n\t\t\tdone: true,\n\t\t}),\n\t}),\n};\n\nconst clientFrozenDeltaStream: IClient = {\n\tmode: \"read\",\n\tdetails: { capabilities: { interactive: true } },\n\tpermission: [],\n\tuser: { id: \"storage-only client\" }, // we need some \"fake\" ID here.\n\tscopes: [],\n};\n\nconst clientIdFrozenDeltaStream: string = \"storage-only client\";\n\n// Cast rationale: ITokenClaims requires tenantId/documentId/user/iat/exp/ver, but a frozen\n// delta stream has no tenant or session to draw real values from — it's a synthetic\n// in-process connection that never reaches a service. Inventing sentinel values would imply\n// quorum membership we cannot honor; only `scopes` actually drives behavior here (DocRead vs\n// DocWrite gates readOnlyInfo). The cast is the honest representation of \"this connection\n// has no claims worth populating.\"\n/* eslint-disable @typescript-eslint/consistent-type-assertions */\nconst readOnlyClaims: ITokenClaims = { scopes: [ScopeType.DocRead] } as ITokenClaims;\nconst writableClaims: ITokenClaims = {\n\tscopes: [ScopeType.DocRead, ScopeType.DocWrite],\n} as ITokenClaims;\n/* eslint-enable @typescript-eslint/consistent-type-assertions */\n\n/**\n * Inert `IDocumentDeltaConnection` for frozen container loads. Has no server upstream:\n * op and signal streams are empty, and `initialClients` contains only its own synthetic\n * read-only client — which lets the connection state handler observe \"self\" in the audience\n * and transition the container to Connected without waiting for a real join op or signal.\n *\n * Two concrete variants share this base — see their JSDoc for variant-specific details:\n *\n * - {@link FrozenDeltaStream} — read-only.\n * - {@link WritableFrozenDeltaStream} — writable.\n *\n * Both variants nack any incoming `submit`: this connection has no upstream and\n * `ConnectionManager.sendMessages` recognizes `WritableFrozenDeltaStream` and drops messages\n * before they reach `submit`, so under normal flow it should never fire. A nack reaching the\n * connectionManager surfaces the misuse — and may close the container — which is the right\n * defensive signal that something has bypassed the expected flow.\n *\n * `submitSignal` is a silent no-op for both variants. Signals are ephemeral and best-effort —\n * runtime/presence subsystems may submit them at any point in the writable-frozen lifetime, and\n * dropping them is the correct behavior here (we have no upstream). Closing the container or\n * triggering a reconnect on a stray signal would be strictly worse than dropping it.\n */\nabstract class FrozenDeltaStreamBase\n\textends TypedEventEmitter<IDocumentDeltaConnectionEvents>\n\timplements IDocumentDeltaConnection, IDisposable\n{\n\tpublic readonly clientId: string;\n\tpublic readonly claims: ITokenClaims;\n\tpublic readonly initialClients: ISignalClient[];\n\tpublic readonly mode: ConnectionMode = \"read\";\n\tpublic readonly existing: boolean = true;\n\tpublic readonly maxMessageSize: number = 0;\n\tpublic readonly version: string = \"\";\n\tpublic readonly initialMessages: ISequencedDocumentMessage[] = [];\n\tpublic readonly initialSignals: ISignalMessage[] = [];\n\tpublic readonly serviceConfiguration: IClientConfiguration = {\n\t\tmaxMessageSize: 0,\n\t\tblockSize: 0,\n\t};\n\tpublic readonly checkpointSequenceNumber?: number | undefined = undefined;\n\n\tconstructor(clientId: string, claims: ITokenClaims) {\n\t\tsuper();\n\t\tthis.clientId = clientId;\n\t\tthis.claims = claims;\n\t\t// initialClients mirrors clientId so the audience handler observes \"self\" and\n\t\t// transitions the container to Connected without waiting for a real join op or signal.\n\t\tthis.initialClients = [{ client: clientFrozenDeltaStream, clientId }];\n\t}\n\n\tsubmit(messages: IDocumentMessage[]): void {\n\t\t// Defensive nack: nothing should send on a frozen delta stream. If this fires, an\n\t\t// invariant in connectionManager has changed and we want it to surface loudly.\n\t\tthis.emit(\n\t\t\t\"nack\",\n\t\t\tthis.clientId,\n\t\t\tmessages.map((operation) => ({\n\t\t\t\toperation,\n\t\t\t\tcontent: { message: \"Cannot submit on a frozen delta stream\", code: 403 },\n\t\t\t})),\n\t\t);\n\t}\n\n\tsubmitSignal(_message: unknown): void {\n\t\t// Intentional no-op. See class JSDoc for rationale.\n\t}\n\n\tprivate _disposed = false;\n\tpublic get disposed(): boolean {\n\t\treturn this._disposed;\n\t}\n\tpublic dispose(): void {\n\t\tthis._disposed = true;\n\t}\n}\n\n/**\n * Read-only variant of {@link FrozenDeltaStreamBase}. Claims show only `DocRead`. Used by\n * storage-only loads (where `connectionManager` synthesizes one directly via\n * `policies.storageOnly`) and by the forbidden / out-of-storage fallback paths.\n * {@link isFrozenDeltaStreamConnection} matches this variant and drives the read-only forcing\n * in `ConnectionManager.readOnlyInfo`. Uses the historical `\"storage-only client\"` constant\n * `clientId`, preserving existing behavior for any consumer that keys off it.\n *\n * `storageOnlyReason` and `readonlyConnectionReason` are surfaced through `IContainer.readOnlyInfo`\n * for diagnostics on the fallback paths (`isDeltaStreamConnectionForbiddenError`,\n * `outOfStorageError`).\n */\nexport class FrozenDeltaStream extends FrozenDeltaStreamBase {\n\tpublic readonly storageOnlyReason: string | undefined;\n\tpublic readonly readonlyConnectionReason: IConnectionStateChangeReason | undefined;\n\n\tconstructor(options?: {\n\t\tstorageOnlyReason?: string;\n\t\treadonlyConnectionReason?: IConnectionStateChangeReason;\n\t}) {\n\t\t// Constant clientId: preserves the pre-PR `\"storage-only client\"` identity for any\n\t\t// consumer that keys off it. The 0x173 replay-assert risk that motivates per-instance\n\t\t// clientIds applies only to the writable variant, where the runtime accumulates dirty\n\t\t// pending ops across reconnects; the read-only variant does not.\n\t\tsuper(clientIdFrozenDeltaStream, readOnlyClaims);\n\t\tthis.storageOnlyReason = options?.storageOnlyReason;\n\t\tthis.readonlyConnectionReason = options?.readonlyConnectionReason;\n\t}\n}\n\n/**\n * Variant of {@link FrozenDeltaStreamBase} that appears to support writing but remains\n * \"frozen\" — no messages are actually sent or received. The stream itself does not enforce\n * the no-send guarantee; that lives in `ConnectionManager.sendMessages`, which recognizes\n * any `WritableFrozenDeltaStream` (via {@link isWritableFrozenDeltaStreamConnection}) and\n * short-circuits before its read-mode upgrade branch. Submitted ops are dropped at the\n * connection-manager layer, so the container stays `Connected` and the runtime accumulates\n * them in `pendingStateManager`.\n *\n * \"Appears writable\" mechanics: claims include `DocWrite` so the container surfaces as\n * writable; not matched by {@link isFrozenDeltaStreamConnection}, so `readOnlyInfo` reports\n * `readonly: false`. Connection mode stays `\"read\"` (advertising `\"write\"` would imply quorum\n * membership we cannot honor).\n *\n * Each instance mints a fresh `frozen-delta-stream/<uuid>` `clientId` to avoid\n * `pendingStateManager` `0x173` (`replayPendingStates called twice for same clientId!`) on\n * reconnect with dirty pending ops. Sibling (not subclass) of `FrozenDeltaStream` so\n * `instanceof` cleanly distinguishes the two for `ConnectionManager`'s short-circuits.\n */\nexport class WritableFrozenDeltaStream extends FrozenDeltaStreamBase {\n\tconstructor() {\n\t\tsuper(`frozen-delta-stream/${uuid()}`, writableClaims);\n\t}\n}\n\n/**\n * Recognizes the read-only variant of {@link FrozenDeltaStreamBase}. Drives the storage-only\n * forcing in `ConnectionManager.readOnlyInfo`: only the read-only variant should make the\n * container surface as read-only. {@link WritableFrozenDeltaStream} is a sibling class, not\n * a subclass, so `instanceof FrozenDeltaStream` already excludes it.\n */\nexport function isFrozenDeltaStreamConnection(\n\tconnection: unknown,\n): connection is FrozenDeltaStream {\n\treturn connection instanceof FrozenDeltaStream;\n}\n\n/**\n * Recognizes the writable variant of {@link FrozenDeltaStreamBase}. Drives the\n * `ConnectionManager.sendMessages` short-circuit: writable-frozen submits must be dropped at\n * the network layer instead of triggering a read→write reconnect. Sibling (not subclass) of\n * {@link FrozenDeltaStream}, so `instanceof WritableFrozenDeltaStream` excludes the read-only\n * variant.\n */\nexport function isWritableFrozenDeltaStreamConnection(\n\tconnection: unknown,\n): connection is WritableFrozenDeltaStream {\n\treturn connection instanceof WritableFrozenDeltaStream;\n}\n"]}