openrtc-file-transfer 2.9.6 → 2.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,3 +53,121 @@ constructs a hidden client or activates a second avenue. It also never caches a
53
53
  transport, dials a peer, or promotes a route. The
54
54
  consumer keeps durable product intent pending until the receiver confirms the
55
55
  completed size and integrity.
56
+
57
+ ### Durable accepted sources
58
+
59
+ The routed `sendAccepted(deviceId, { header, openSource }, options)` source API
60
+ accepts the product's persisted immutable header, including SHA-256. It first
61
+ reconciles the offer with the authenticated receiver. A committed completion
62
+ receipt returns without opening the local file; otherwise missing bytes cause
63
+ one lazy source open and bounded, abort-aware revision verification before body
64
+ transmission. The product owns source storage, authorization, queue state and
65
+ receipt persistence. No connection or additional protocol handler is created.
66
+ The legacy stream implementation rejects this API before sending bytes. Consumers
67
+ of older published packages must detect API availability and retain their prior
68
+ source-first path until qualified artifacts are adopted.
69
+
70
+ ### Receive pacing and deadline renewal
71
+
72
+ Routed message and segmented-stream receivers may use `beforeReceiveBody` to
73
+ admit each authenticated body attempt into a product rate/quota controller.
74
+ It receives transfer ID, remote node ID, offset, byte count and an abort signal.
75
+ Duplicate body attempts consume policy bytes without advancing logical progress
76
+ or writing the same prefix twice. Control frames bypass body policy. Payload
77
+ attempts are application bytes, not an exact physical-network counter.
78
+
79
+ Receiver pacing requires the immutable offer metadata
80
+ `openrtc.receive-wait: "1"`. Fresh routed `send` offers advertise this capability;
81
+ call `withReceiverWaitCapability(header)` **before** persisting a new accepted
82
+ offer. `sendAccepted` preserves that exact persisted header, including metadata.
83
+ A paced receiver rejects a legacy offer before allocating its sink or reading
84
+ source bytes. A product-confirmed completion receipt still returns body-free,
85
+ without running a body policy or requiring the legacy source to exist.
86
+
87
+ `BULK_RECEIVE_FLOW_CONTROL_VERSION === 1` also supports per-sink `bodyPolicy`.
88
+ Use its captured execution `signal`, `beforeAttempt` callback and
89
+ `requiresReceiverWait` admission flag when product storage owns each job's
90
+ policy/lease. The runtime callback still runs first; a sink cannot bypass it.
91
+ A sink with `requiresReceiverWait: false` admits old offers without sending
92
+ unsupported wait controls. Its callback must remain unpaced for such an offer
93
+ or reject if a later policy requires waiting. Required waits reject before body
94
+ acceptance and abort any prepared sink. Product receipts are checked first.
95
+ Signal retirement aborts active policy work and releases idle receive sinks.
96
+ Stream/v1 cannot host this body policy and rejects it before reading body data.
97
+
98
+ OFT2 kind `0x08` is a waiting control frame: transfer ID, big-endian u64 last
99
+ checkpointed offset and big-endian u32 lease milliseconds (1–60,000). The
100
+ receiver renews its configurable lease (default 10,000 ms) during body policy,
101
+ sink IO/checkpointing and final verification/publication. The sender accepts
102
+ renewal only from the expected authenticated peer, for the active transfer,
103
+ within its current body window and only when the offer negotiated support.
104
+ Waiting neither ACKs bytes nor confirms delivery. If renewal stops, the current
105
+ lease expires and the normal bounded recovery applies. Application expiry and
106
+ execution permission remain the product's responsibility.
107
+
108
+ Caller cancellation, receiver subscription stop/replacement and protocol
109
+ disposal abort active policy work and stop renewal. Hooks must honor the abort
110
+ signal; durable sinks must retain their product execution/commit fences. A sink
111
+ that finishes creation after execution retirement is aborted rather than
112
+ accepted. Legacy raw-stream framing rejects receive pacing; native consumers
113
+ must qualify and implement the same negotiated deadline behavior before
114
+ advertising the capability. Neither waiting nor body policy changes OpenRTC's
115
+ connection, admission or route ownership.
116
+
117
+ ### Required capabilities and large-file admission
118
+
119
+ For routed message or segmented-stream transfers, `send` accepts
120
+ `requiredCapabilities: { durableResume?, completionReceipt?, extensions? }`.
121
+ Accepted jobs instead call `withRequiredCapabilities(header, requirements)` before
122
+ persisting their immutable header; `sendAccepted` preserves that exact metadata.
123
+ A requirement is encoded under `openrtc.required-capabilities.v1` as a closed
124
+ version-1 JSON document. Unknown fields, duplicate or invalid extension names,
125
+ and unsupported versions fail closed. Extensions are consumer-owned names; they
126
+ must describe real receiver authorization/storage behavior rather than silently
127
+ changing generic delivery into a managed destination operation.
128
+
129
+ The sender probes before offering bytes whenever the header has requirements,
130
+ exceeds the 256 MiB legacy buffered ceiling, or an accepted job exceeds its
131
+ current sender size limit. An unsupported verdict rejects before opening an
132
+ accepted source or allocating a receive sink. A legacy peer that ignores the
133
+ probe produces an actionable error after the existing bounded protocol retries;
134
+ OpenRTC still owns admission, routes and connection recovery. Explicit stream/v1
135
+ send and receive reject required-feature metadata before accepting a body.
136
+
137
+ The receiver derives its admitted maximum from `maxFileBytes` and the sink:
138
+ without a custom streaming sink it never advertises above 256 MiB. With one,
139
+ `receiveCapabilities` may declare durable resume and application extensions.
140
+ A durable-resume claim requires a checkpointing sink; the application must make
141
+ those checkpoints durable and verify reopened prefixes. `completionReceipt`
142
+ support requires the product `hasCompletionReceipt(header, authenticatedPeer)`
143
+ callback. When a job requires receipts, completion checks that callback after
144
+ publication/commit and rejects success if the receipt is still missing. Neither
145
+ the SDK's in-memory completed map nor a capability declaration is durable proof.
146
+ An absent or aborted receive subscription advertises `receiveAvailable: false`,
147
+ including for zero-byte files.
148
+
149
+ OFT2 kind `0x09` carries a capability request: transfer ID, big-endian u32 header
150
+ length and optional immutable JSON header (zero length for a general query).
151
+ Kind `0x0a` carries a big-endian u32 JSON length and a closed version-1 response
152
+ bounded to 4096 bytes: `version`, `durableResume`, `completionReceipt`,
153
+ `extensions`, `receiveAvailable`, and `maxFileBytes` (a nonnegative safe integer).
154
+ Header probes retain the configured header-byte bound but validate size against
155
+ the JavaScript safe-integer ceiling, independently of current body admission.
156
+
157
+ Before returning capabilities, a header probe checks the authenticated product
158
+ receipt. A matching receipt returns the existing `Completed` frame, even if
159
+ receiving is disabled or current file limits have decreased. An accepted-source
160
+ sender likewise reconciles that receipt before enforcing its current body limit,
161
+ without reopening or resending the source. Immutable header conflicts fail
162
+ closed; replies must match the intended authenticated peer and transfer ID.
163
+ Native codec support does not by itself implement or advertise these execution,
164
+ application extension, authorization, or persistence guarantees.
165
+
166
+ The staged `BULK_JOB_FLOW_CONTROL_VERSION = 1` contract also accepts
167
+ `beforeSendBody` on an individual `send` or `sendAccepted` call. The runtime
168
+ callback and job callback both run before each body attempt, outside the ACK
169
+ deadline; a job callback cannot override a runtime restriction. Product owners
170
+ retain authorization, execution cancellation, durable counters and policy state.
171
+ Persist `BulkFlowController.snapshot()` before allowing body work and restore it
172
+ into a fresh controller when a durable execution owner changes. Publication and
173
+ consumer capability checks are required before relying on these staged APIs.
@@ -0,0 +1,25 @@
1
+ import type { FileTransferHeader } from './index.js';
2
+ export declare const REQUIRED_CAPABILITIES_METADATA_KEY = "openrtc.required-capabilities.v1";
3
+ export declare const MAX_CAPABILITY_BYTES = 4096;
4
+ export declare const LEGACY_MAX_FILE_BYTES: number;
5
+ export interface TransferRequirements {
6
+ durableResume?: boolean;
7
+ completionReceipt?: boolean;
8
+ extensions?: readonly string[];
9
+ }
10
+ export interface RequiredTransferCapabilities {
11
+ version: 1;
12
+ durableResume: boolean;
13
+ completionReceipt: boolean;
14
+ extensions: string[];
15
+ }
16
+ export interface TransferCapabilities extends RequiredTransferCapabilities {
17
+ receiveAvailable: boolean;
18
+ maxFileBytes: number;
19
+ }
20
+ export declare function requiredCapabilities(value: unknown, wire?: boolean): RequiredTransferCapabilities;
21
+ export declare function transferCapabilities(value: unknown): TransferCapabilities;
22
+ export declare function requiredTransferCapabilities(header: FileTransferHeader): RequiredTransferCapabilities | null;
23
+ export declare function withRequiredCapabilities(header: FileTransferHeader, requirements: TransferRequirements): FileTransferHeader;
24
+ export declare function requireCapabilities(capabilities: TransferCapabilities, header: FileTransferHeader): void;
25
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,eAAO,MAAM,kCAAkC,qCAAqC,CAAC;AACrF,eAAO,MAAM,oBAAoB,OAAO,CAAC;AACzC,eAAO,MAAM,qBAAqB,QAAoB,CAAC;AAEvD,MAAM,WAAW,oBAAoB;IACjC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AACD,MAAM,WAAW,4BAA4B;IACzC,OAAO,EAAE,CAAC,CAAC;IAAC,aAAa,EAAE,OAAO,CAAC;IAAC,iBAAiB,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAC;CACxF;AACD,MAAM,WAAW,oBAAqB,SAAQ,4BAA4B;IAAG,gBAAgB,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE;AAe9H,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,UAAQ,GAAG,4BAA4B,CAU/F;AACD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,CAMzE;AACD,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,kBAAkB,GAAG,4BAA4B,GAAG,IAAI,CAM5G;AACD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,EAAE,YAAY,EAAE,oBAAoB,GAAG,kBAAkB,CAM3H;AACD,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,oBAAoB,EAAE,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAMxG"}
@@ -0,0 +1,58 @@
1
+ export const REQUIRED_CAPABILITIES_METADATA_KEY = 'openrtc.required-capabilities.v1';
2
+ export const MAX_CAPABILITY_BYTES = 4096;
3
+ export const LEGACY_MAX_FILE_BYTES = 256 * 1024 * 1024;
4
+ function record(value, keys) {
5
+ if (!value || typeof value !== 'object' || Array.isArray(value)
6
+ || Object.keys(value).some(key => !keys.includes(key)))
7
+ throw new Error('invalid transfer capability fields');
8
+ return value;
9
+ }
10
+ function extensions(value) {
11
+ if (!Array.isArray(value) || value.length > 16 || value.some(item => typeof item !== 'string' || item.length > 128 || !/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.test(item)))
12
+ throw new Error('invalid transfer capability extensions');
13
+ const sorted = [...value].sort();
14
+ if (sorted.some((item, index) => index > 0 && item === sorted[index - 1]))
15
+ throw new Error('duplicate transfer capability extension');
16
+ return sorted;
17
+ }
18
+ export function requiredCapabilities(value, wire = false) {
19
+ const row = record(value, ['version', 'durableResume', 'completionReceipt', 'extensions']);
20
+ if (wire ? row.version !== 1 || typeof row.durableResume !== 'boolean'
21
+ || typeof row.completionReceipt !== 'boolean' || row.extensions === undefined
22
+ : row.version !== undefined && row.version !== 1
23
+ || row.durableResume !== undefined && typeof row.durableResume !== 'boolean'
24
+ || row.completionReceipt !== undefined && typeof row.completionReceipt !== 'boolean')
25
+ throw new Error('invalid transfer capability version or flags');
26
+ return { version: 1, durableResume: row.durableResume === true, completionReceipt: row.completionReceipt === true,
27
+ extensions: extensions(row.extensions ?? []) };
28
+ }
29
+ export function transferCapabilities(value) {
30
+ const row = record(value, ['version', 'maxFileBytes', 'receiveAvailable', 'durableResume', 'completionReceipt', 'extensions']);
31
+ if (typeof row.receiveAvailable !== 'boolean' || !Number.isSafeInteger(row.maxFileBytes) || row.maxFileBytes < 0)
32
+ throw new Error('invalid transfer capability file limit');
33
+ return { ...requiredCapabilities({ version: row.version, durableResume: row.durableResume,
34
+ completionReceipt: row.completionReceipt, extensions: row.extensions }, true), receiveAvailable: row.receiveAvailable, maxFileBytes: row.maxFileBytes };
35
+ }
36
+ export function requiredTransferCapabilities(header) {
37
+ const encoded = header.metadata?.[REQUIRED_CAPABILITIES_METADATA_KEY];
38
+ if (encoded === undefined)
39
+ return null;
40
+ if (typeof encoded !== 'string' || new TextEncoder().encode(encoded).byteLength > MAX_CAPABILITY_BYTES)
41
+ throw new Error('invalid required transfer capability metadata');
42
+ return requiredCapabilities(JSON.parse(encoded), true);
43
+ }
44
+ export function withRequiredCapabilities(header, requirements) {
45
+ const encoded = JSON.stringify(requiredCapabilities(requirements));
46
+ const old = header.metadata?.[REQUIRED_CAPABILITIES_METADATA_KEY];
47
+ if (old !== undefined && old !== encoded)
48
+ throw new Error('transfer_identity_conflict: required transfer capabilities differ from the accepted header');
49
+ return { ...header, metadata: { ...header.metadata, [REQUIRED_CAPABILITIES_METADATA_KEY]: encoded } };
50
+ }
51
+ export function requireCapabilities(capabilities, header) {
52
+ const required = requiredTransferCapabilities(header);
53
+ if (!capabilities.receiveAvailable || header.size > capabilities.maxFileBytes || required?.durableResume && !capabilities.durableResume
54
+ || required?.completionReceipt && !capabilities.completionReceipt
55
+ || required?.extensions.some(extension => !capabilities.extensions.includes(extension)))
56
+ throw new Error('transfer_capability_required: receiver cannot admit the selected size or required transfer features');
57
+ }
58
+ //# sourceMappingURL=capabilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,kCAAkC,GAAG,kCAAkC,CAAC;AACrF,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC;AACzC,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAYvD,SAAS,MAAM,CAAC,KAAc,EAAE,IAAc;IAC1C,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;WACxD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;IAClH,OAAO,KAAgC,CAAC;AAC5C,CAAC;AACD,SAAS,UAAU,CAAC,KAAc;IAC9B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAChE,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG,IAAI,CAAC,qCAAqC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnG,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,EAAc,CAAC;IAC7C,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,IAAI,IAAI,KAAK,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IACtI,OAAO,MAAM,CAAC;AAClB,CAAC;AACD,MAAM,UAAU,oBAAoB,CAAC,KAAc,EAAE,IAAI,GAAG,KAAK;IAC7D,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,eAAe,EAAE,mBAAmB,EAAE,YAAY,CAAC,CAAC,CAAC;IAC3F,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,KAAK,CAAC,IAAI,OAAO,GAAG,CAAC,aAAa,KAAK,SAAS;WAC/D,OAAO,GAAG,CAAC,iBAAiB,KAAK,SAAS,IAAI,GAAG,CAAC,UAAU,KAAK,SAAS;QAC7E,CAAC,CAAC,GAAG,CAAC,OAAO,KAAK,SAAS,IAAI,GAAG,CAAC,OAAO,KAAK,CAAC;eACzC,GAAG,CAAC,aAAa,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,aAAa,KAAK,SAAS;eACzE,GAAG,CAAC,iBAAiB,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,iBAAiB,KAAK,SAAS;QACxF,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IACpE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,aAAa,EAAE,GAAG,CAAC,aAAa,KAAK,IAAI,EAAE,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,KAAK,IAAI;QAC7G,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,CAAC;AACvD,CAAC;AACD,MAAM,UAAU,oBAAoB,CAAC,KAAc;IAC/C,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,cAAc,EAAE,kBAAkB,EAAE,eAAe,EAAE,mBAAmB,EAAE,YAAY,CAAC,CAAC,CAAC;IAC/H,IAAI,OAAO,GAAG,CAAC,gBAAgB,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,YAAY,CAAC,IAAK,GAAG,CAAC,YAAuB,GAAG,CAAC;QACxH,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC9D,OAAO,EAAE,GAAG,oBAAoB,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,CAAC,aAAa;YACrF,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,EAAE,IAAI,CAAC,EAAE,gBAAgB,EAAE,GAAG,CAAC,gBAAgB,EAAE,YAAY,EAAE,GAAG,CAAC,YAAsB,EAAE,CAAC;AAC1K,CAAC;AACD,MAAM,UAAU,4BAA4B,CAAC,MAA0B;IACnE,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC,kCAAkC,CAAC,CAAC;IACtE,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACvC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,GAAG,oBAAoB;QAClG,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACrE,OAAO,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC;AAC3D,CAAC;AACD,MAAM,UAAU,wBAAwB,CAAC,MAA0B,EAAE,YAAkC;IACnG,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,YAAY,CAAC,CAAC,CAAC;IACnE,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC,kCAAkC,CAAC,CAAC;IAClE,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,OAAO;QACpC,MAAM,IAAI,KAAK,CAAC,4FAA4F,CAAC,CAAC;IAClH,OAAO,EAAE,GAAG,MAAM,EAAE,QAAQ,EAAE,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC,kCAAkC,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC;AAC1G,CAAC;AACD,MAAM,UAAU,mBAAmB,CAAC,YAAkC,EAAE,MAA0B;IAC9F,MAAM,QAAQ,GAAG,4BAA4B,CAAC,MAAM,CAAC,CAAC;IACtD,IAAI,CAAC,YAAY,CAAC,gBAAgB,IAAI,MAAM,CAAC,IAAI,GAAG,YAAY,CAAC,YAAY,IAAI,QAAQ,EAAE,aAAa,IAAI,CAAC,YAAY,CAAC,aAAa;WAChI,QAAQ,EAAE,iBAAiB,IAAI,CAAC,YAAY,CAAC,iBAAiB;WAC9D,QAAQ,EAAE,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,YAAY,CAAC,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QACvF,MAAM,IAAI,KAAK,CAAC,qGAAqG,CAAC,CAAC;AAC/H,CAAC"}
@@ -0,0 +1,45 @@
1
+ /** Application bulk-body pacing only. Admission, ACKs, and lifecycle messages
2
+ * bypass this controller. Share one instance across all bulk work in a runtime. */
3
+ export type BulkDirection = 'upload' | 'download';
4
+ export interface BulkRates {
5
+ uploadBytesPerSecond: number | null;
6
+ downloadBytesPerSecond: number | null;
7
+ }
8
+ export interface BulkFlowSnapshot {
9
+ version: 1;
10
+ burstBytes: number;
11
+ upload: {
12
+ rate: number | null;
13
+ tokens: number;
14
+ };
15
+ download: {
16
+ rate: number | null;
17
+ tokens: number;
18
+ };
19
+ }
20
+ export declare class BulkFlowController {
21
+ readonly burstBytes: number;
22
+ private readonly maxPending;
23
+ private readonly states;
24
+ private readonly changes;
25
+ private pending;
26
+ private used;
27
+ constructor(rates?: BulkRates, burstBytes?: number, maxPending?: number);
28
+ configure(rates: BulkRates): void;
29
+ /** Product storage owns durability and wall-clock elapsed time. Export
30
+ * after acquisition and persist before allowing the body attempt. Pending
31
+ * requests are not grants and are deliberately excluded. */
32
+ snapshot(): BulkFlowSnapshot;
33
+ /** Restore into a fresh owner only; switching execution owners must not
34
+ * mint a new burst. The caller supplies conservative offline elapsed time. */
35
+ restore(raw: unknown, elapsedMs: number): void;
36
+ /** Wait before submitting a bounded body slice to a transport. Control
37
+ * deadlines start after this wait; cancellation never authorizes a slice. */
38
+ acquire(direction: BulkDirection, peer: string, bytes: number, signal?: AbortSignal): Promise<void>;
39
+ /** Admit all applicable budgets synchronously, before any token debit.
40
+ * Product owners retain/persist these controllers across job handoff. */
41
+ acquireWith(direction: BulkDirection, peer: string, bytes: number, constraints: readonly BulkFlowController[], signal?: AbortSignal): Promise<void>;
42
+ private refill;
43
+ private pump;
44
+ }
45
+ //# sourceMappingURL=flowControl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flowControl.d.ts","sourceRoot":"","sources":["../src/flowControl.ts"],"names":[],"mappings":"AAAA;mFACmF;AACnF,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,UAAU,CAAC;AAClD,MAAM,WAAW,SAAS;IAAG,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE;AACzG,MAAM,WAAW,gBAAgB;IAC7B,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD;AAMD,qBAAa,kBAAkB;IAMvB,QAAQ,CAAC,UAAU;IAAc,OAAO,CAAC,QAAQ,CAAC,UAAU;IALhE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwC;IAC/D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsE;IAC9F,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,IAAI,CAAS;gBACT,KAAK,GAAE,SAAwE,EAC9E,UAAU,SAAY,EAAmB,UAAU,SAAO;IASvE,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI;IAcjC;;gEAE4D;IAC5D,QAAQ,IAAI,gBAAgB;IAM5B;kFAC8E;IAC9E,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAsB9C;iFAC6E;IAC7E,OAAO,CAAC,SAAS,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAGnG;6EACyE;IACzE,WAAW,CAAC,SAAS,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAC7D,WAAW,EAAE,SAAS,kBAAkB,EAAE,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAuCpF,OAAO,CAAC,MAAM;IAMd,OAAO,CAAC,IAAI;CA4Cf"}
@@ -0,0 +1,192 @@
1
+ export class BulkFlowController {
2
+ constructor(rates = { uploadBytesPerSecond: null, downloadBytesPerSecond: null }, burstBytes = 64 * 1024, maxPending = 4096) {
3
+ this.burstBytes = burstBytes;
4
+ this.maxPending = maxPending;
5
+ this.changes = { upload: new Set(), download: new Set() };
6
+ this.pending = 0;
7
+ this.used = false;
8
+ if (!Number.isSafeInteger(burstBytes) || burstBytes < 1 || burstBytes > 64 * 1024
9
+ || !Number.isSafeInteger(maxPending) || maxPending < 1 || maxPending > 4096)
10
+ throw new Error('Invalid bulk flow resource bound');
11
+ const state = () => ({ rate: null, tokens: burstBytes,
12
+ updatedAt: performance.now(), peers: new Map(), order: [] });
13
+ this.states = { upload: state(), download: state() };
14
+ this.configure(rates);
15
+ }
16
+ configure(rates) {
17
+ for (const direction of ['upload', 'download']) {
18
+ const rate = rates[`${direction}BytesPerSecond`];
19
+ if (rate !== null && (!Number.isSafeInteger(rate) || rate <= 0))
20
+ throw new Error('Bulk rate must be unset or a positive safe integer');
21
+ }
22
+ for (const direction of ['upload', 'download']) {
23
+ const state = this.states[direction];
24
+ this.refill(state);
25
+ state.rate = rates[`${direction}BytesPerSecond`];
26
+ this.pump(direction);
27
+ for (const changed of this.changes[direction])
28
+ changed();
29
+ }
30
+ }
31
+ /** Product storage owns durability and wall-clock elapsed time. Export
32
+ * after acquisition and persist before allowing the body attempt. Pending
33
+ * requests are not grants and are deliberately excluded. */
34
+ snapshot() {
35
+ this.refill(this.states.upload);
36
+ this.refill(this.states.download);
37
+ const value = (direction) => ({ rate: this.states[direction].rate,
38
+ tokens: this.states[direction].tokens });
39
+ return { version: 1, burstBytes: this.burstBytes, upload: value('upload'), download: value('download') };
40
+ }
41
+ /** Restore into a fresh owner only; switching execution owners must not
42
+ * mint a new burst. The caller supplies conservative offline elapsed time. */
43
+ restore(raw, elapsedMs) {
44
+ if (this.used || !Number.isFinite(elapsedMs) || elapsedMs < 0)
45
+ throw new Error('Invalid bulk flow restoration');
46
+ const saved = raw;
47
+ if (saved?.version !== 1 || saved.burstBytes !== this.burstBytes)
48
+ throw new Error('Unsupported bulk flow state');
49
+ for (const direction of ['upload', 'download']) {
50
+ const value = saved[direction];
51
+ if (!value || (value.rate !== null && (!Number.isSafeInteger(value.rate) || value.rate <= 0))
52
+ || !Number.isFinite(value.tokens) || value.tokens < 0 || value.tokens > this.burstBytes)
53
+ throw new Error('Invalid bulk flow state');
54
+ }
55
+ const now = performance.now();
56
+ for (const direction of ['upload', 'download']) {
57
+ const value = saved[direction], state = this.states[direction];
58
+ state.rate = value.rate;
59
+ state.tokens = value.rate === null ? value.tokens : Math.min(this.burstBytes, value.tokens + elapsedMs * value.rate / 1000);
60
+ state.updatedAt = now;
61
+ }
62
+ this.used = true;
63
+ }
64
+ /** Wait before submitting a bounded body slice to a transport. Control
65
+ * deadlines start after this wait; cancellation never authorizes a slice. */
66
+ acquire(direction, peer, bytes, signal) {
67
+ return this.acquireWith(direction, peer, bytes, [], signal);
68
+ }
69
+ /** Admit all applicable budgets synchronously, before any token debit.
70
+ * Product owners retain/persist these controllers across job handoff. */
71
+ acquireWith(direction, peer, bytes, constraints, signal) {
72
+ if (!Array.isArray(constraints) || constraints.length > 16 || new Set(constraints).size !== constraints.length
73
+ || constraints.some(flow => !(flow instanceof BulkFlowController) || flow === this || bytes > flow.burstBytes))
74
+ return Promise.reject(new Error('Invalid bulk flow constraints'));
75
+ if (!this.states[direction] || !peer || peer.length > 512 || !Number.isSafeInteger(bytes)
76
+ || bytes < 0 || bytes > this.burstBytes)
77
+ return Promise.reject(new Error('Invalid bulk body slice'));
78
+ if (signal?.aborted)
79
+ return Promise.reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
80
+ if (!bytes)
81
+ return Promise.resolve();
82
+ this.used = true;
83
+ for (const flow of constraints)
84
+ flow.used = true;
85
+ if (this.pending >= this.maxPending)
86
+ return Promise.reject(new Error('receiver_busy: bulk flow queue is full'));
87
+ const state = this.states[direction];
88
+ return new Promise((resolve, reject) => {
89
+ const wake = () => this.pump(direction);
90
+ const dependencies = [...constraints];
91
+ for (const flow of dependencies)
92
+ flow.changes[direction].add(wake);
93
+ const request = { bytes, resolve, reject, signal, constraints: dependencies,
94
+ release: () => { for (const flow of dependencies)
95
+ flow.changes[direction].delete(wake); } };
96
+ const queue = state.peers.get(peer) ?? [];
97
+ if (!queue.length) {
98
+ state.peers.set(peer, queue);
99
+ state.order.push(peer);
100
+ }
101
+ queue.push(request);
102
+ this.pending++;
103
+ if (signal) {
104
+ request.abort = () => {
105
+ const index = queue.indexOf(request);
106
+ if (index < 0)
107
+ return;
108
+ queue.splice(index, 1);
109
+ this.pending--;
110
+ signal.removeEventListener('abort', request.abort);
111
+ if (!queue.length) {
112
+ state.peers.delete(peer);
113
+ state.order = state.order.filter(value => value !== peer);
114
+ }
115
+ request.release();
116
+ reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
117
+ this.pump(direction);
118
+ };
119
+ signal.addEventListener('abort', request.abort, { once: true });
120
+ }
121
+ this.pump(direction);
122
+ });
123
+ }
124
+ refill(state) {
125
+ const now = performance.now();
126
+ if (state.rate !== null)
127
+ state.tokens = Math.min(this.burstBytes, state.tokens + Math.max(0, now - state.updatedAt) * state.rate / 1000);
128
+ state.updatedAt = now;
129
+ }
130
+ pump(direction) {
131
+ const state = this.states[direction];
132
+ if (state.timer !== undefined) {
133
+ clearTimeout(state.timer);
134
+ state.timer = undefined;
135
+ }
136
+ this.refill(state);
137
+ let blocked = 0, nextWait;
138
+ while (state.order.length && blocked < state.order.length) {
139
+ const peer = state.order[0], queue = state.peers.get(peer);
140
+ const request = queue[0];
141
+ const globalWait = state.rate !== null && state.tokens + 1e-9 < request.bytes
142
+ ? (request.bytes - state.tokens) * 1000 / state.rate : undefined;
143
+ let constraintWait;
144
+ for (const flow of request.constraints) {
145
+ const child = flow.states[direction];
146
+ flow.refill(child);
147
+ if (child.rate !== null && child.tokens + 1e-9 < request.bytes) {
148
+ const wait = (request.bytes - child.tokens) * 1000 / child.rate;
149
+ constraintWait = Math.max(constraintWait ?? 0, wait);
150
+ }
151
+ }
152
+ if (constraintWait === undefined && globalWait !== undefined) {
153
+ // An aggregate-ready large slice keeps its turn; small slices
154
+ // cannot repeatedly consume its earned global credit.
155
+ nextWait = Math.min(nextWait ?? Infinity, globalWait);
156
+ break;
157
+ }
158
+ if (constraintWait !== undefined) {
159
+ // A job-blocked slice reserves nothing. Rotate only this
160
+ // ineligible peer so unrelated ready work can use its turn.
161
+ nextWait = Math.min(nextWait ?? Infinity, Math.max(globalWait ?? 0, constraintWait));
162
+ state.order.push(state.order.shift());
163
+ blocked++;
164
+ continue;
165
+ }
166
+ if (state.rate !== null)
167
+ state.tokens = Math.max(0, state.tokens - request.bytes);
168
+ for (const flow of request.constraints) {
169
+ const child = flow.states[direction];
170
+ if (child.rate !== null)
171
+ child.tokens = Math.max(0, child.tokens - request.bytes);
172
+ }
173
+ queue.shift();
174
+ state.order.shift();
175
+ this.pending--;
176
+ if (queue.length)
177
+ state.order.push(peer);
178
+ else
179
+ state.peers.delete(peer);
180
+ if (request.abort)
181
+ request.signal?.removeEventListener('abort', request.abort);
182
+ request.release();
183
+ request.resolve();
184
+ blocked = 0;
185
+ }
186
+ if (state.order.length && nextWait !== undefined) {
187
+ const waitMs = Math.max(1, Math.ceil(nextWait));
188
+ state.timer = setTimeout(() => { state.timer = undefined; this.pump(direction); }, Math.min(waitMs, 2147483647));
189
+ }
190
+ }
191
+ }
192
+ //# sourceMappingURL=flowControl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flowControl.js","sourceRoot":"","sources":["../src/flowControl.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,15 @@
1
+ import { type TransferCapabilities, type TransferRequirements } from './capabilities.js';
2
+ export { requiredTransferCapabilities, withRequiredCapabilities, REQUIRED_CAPABILITIES_METADATA_KEY, LEGACY_MAX_FILE_BYTES } from './capabilities.js';
3
+ export type { TransferCapabilities, TransferRequirements, RequiredTransferCapabilities } from './capabilities.js';
1
4
  import type { Avenue } from 'openrtc';
2
5
  import type { ChannelDescriptor, RuntimeClient } from 'openrtc/runtime';
6
+ /** Checkpoint-before-offset and product-commit-before-completion semantics. */
7
+ export declare const DURABLE_DELIVERY_VERSION: 1;
8
+ /** Per-sink body policy, legacy admission and execution cancellation. */
9
+ export declare const BULK_RECEIVE_FLOW_CONTROL_VERSION: 1;
10
+ export declare const BULK_RECEIVE_ADMISSION_WAIT_VERSION: 1;
11
+ /** Unpaced stream/v1 sink policy and idle execution retirement. */
12
+ export declare const BULK_STREAM_RECEIVE_FLOW_CONTROL_VERSION: 1;
3
13
  /** File transfer binds to one already-active 2.0 capability avenue. */
4
14
  export type TransferClient = Pick<Avenue, 'channels' | 'diagnostics' | 'peers'> & {
5
15
  /** Stable, route-level application intake for replaceable connection wrappers. */
@@ -12,6 +22,12 @@ export type TransferClient = Pick<Avenue, 'channels' | 'diagnostics' | 'peers'>
12
22
  */
13
23
  export declare function transferFromRuntime(runtime: RuntimeClient): TransferClient;
14
24
  export declare const CHANNEL: ChannelDescriptor;
25
+ export declare const RECEIVER_WAIT_METADATA_KEY = "openrtc.receive-wait";
26
+ export declare const MAX_RECEIVER_WAIT_LEASE_MS = 60000;
27
+ /** Persist this capability as part of the immutable offer before acceptance. */
28
+ export declare function withReceiverWaitCapability(header: FileTransferHeader): FileTransferHeader;
29
+ export declare const MAX_TRANSFER_CHUNK_BYTES: number;
30
+ export declare const MAX_TRANSFER_WINDOW_CHUNKS = 64;
15
31
  export interface FileTransferHeader {
16
32
  version: 1;
17
33
  transferId: string;
@@ -40,10 +56,22 @@ export interface ReceivedFile {
40
56
  remoteNodeId: string;
41
57
  }
42
58
  export interface ReceiveSink<Result = unknown> {
59
+ /** Product execution policy for this accepted receive. Unpaced legacy
60
+ * offers may opt out of wait negotiation; their callback must not wait for
61
+ * rate credit and must reject if a later policy requires it. */
62
+ bodyPolicy?: {
63
+ requiresReceiverWait: boolean;
64
+ signal: AbortSignal;
65
+ beforeAttempt(attempt: TransferReceiveBodyAttempt): Promise<void>;
66
+ };
43
67
  /** Must equal the requested header offset for a resumed transfer. */
44
68
  resumeOffset?: number;
45
69
  write(chunk: Uint8Array): Promise<void>;
70
+ /** Persist the accepted prefix before the protocol advertises its offset. */
71
+ checkpoint?(): Promise<void>;
46
72
  close(): Promise<Result>;
73
+ /** Commit the product receipt after verification and history publication. */
74
+ commit?(): Promise<void>;
47
75
  abort(reason: unknown): Promise<void>;
48
76
  /** Verify a sender-declared digest without requiring WebView buffering. */
49
77
  verifySha256?(expectedHex: string): Promise<boolean>;
@@ -62,14 +90,63 @@ export interface TransferOptions<Result = ReceivedFile> {
62
90
  maxFileBytes?: number;
63
91
  /** Maximum time to wait for the receiver's persisted-and-verified ACK. */
64
92
  ackTimeoutMs?: number;
65
- createReceiveSink?: (header: FileTransferHeader, remoteNodeId: string) => Promise<ReceiveSink<Result>>;
93
+ createReceiveSink?: (header: FileTransferHeader, remoteNodeId: string, signal?: AbortSignal) => Promise<ReceiveSink<Result>>;
94
+ /** Product-owned durable receipt lookup on the authenticated peer scope. */
95
+ hasCompletionReceipt?: (header: FileTransferHeader, remoteNodeId: string) => Promise<boolean>;
96
+ /** Application aggregate pacing/quota checkpoint before each body attempt.
97
+ * Runs outside the receiver-ACK deadline; headers and controls bypass it. */
98
+ beforeSendBody?: (attempt: TransferBodyAttempt) => Promise<void>;
99
+ /** Routed receivers pace/quota-check body work without granting a durable
100
+ * offset. Legacy offers are refused before sink acceptance unless a retained
101
+ * completion receipt exists. The callback must honor its execution signal. */
102
+ beforeReceiveBody?: (attempt: TransferReceiveBodyAttempt) => Promise<void>;
103
+ /** Each negotiated wait lease is bounded; only live receiver work renews it. */
104
+ receiveWaitLeaseMs?: number;
105
+ /** Claims belong to the product sink/authorization owner, never transport.
106
+ * A durable-resume claim requires an installed streaming receive sink. */
107
+ receiveCapabilities?: {
108
+ durableResume?: boolean;
109
+ extensions?: readonly string[];
110
+ };
66
111
  }
112
+ export interface TransferBodyAttempt {
113
+ /** Source position of this immutable body slice; absent on older adapters. */
114
+ offset?: number;
115
+ transferId: string;
116
+ /** Target peer selector supplied to send(); the runtime resolves identity. */
117
+ peerId: string;
118
+ bytes: number;
119
+ signal?: AbortSignal;
120
+ }
121
+ export interface TransferReceiveBodyAttempt {
122
+ transferId: string;
123
+ remoteNodeId: string;
124
+ offset: number;
125
+ bytes: number;
126
+ signal: AbortSignal;
127
+ }
128
+ export { BulkFlowController } from './flowControl.js';
129
+ export type { BulkDirection, BulkRates, BulkFlowSnapshot } from './flowControl.js';
130
+ export declare const BULK_SEND_FLOW_CONTROL_VERSION = 1;
131
+ export declare const BULK_JOB_FLOW_CONTROL_VERSION = 1;
132
+ export declare const BULK_SCOPED_FLOW_CONTROL_VERSION = 1;
133
+ /** Product-persisted immutable offer. Body access is deferred until the
134
+ * authenticated receiver requests bytes rather than returning its receipt. */
135
+ export interface AcceptedFileSource {
136
+ header: FileTransferHeader;
137
+ openSource(): Promise<File>;
138
+ }
139
+ export type AcceptedSendOptions = Pick<SendFileOptions, 'signal' | 'onProgress' | 'beforeSendBody'>;
67
140
  export interface SendFileOptions {
68
141
  transferId?: string;
142
+ requiredCapabilities?: TransferRequirements;
69
143
  metadata?: Record<string, string>;
70
144
  integrity?: 'none' | 'sha256';
71
145
  offsetBytes?: number;
72
146
  signal?: AbortSignal;
147
+ /** Product execution-specific policy, in addition to the runtime policy.
148
+ * Both complete before a body attempt and outside the receiver ACK timer. */
149
+ beforeSendBody?: (attempt: TransferBodyAttempt) => Promise<void>;
73
150
  onProgress?: (progress: TransferProgress) => void;
74
151
  }
75
152
  export interface ReceiveFileOptions<Result> {
@@ -81,6 +158,14 @@ export interface ReceiveFileOptions<Result> {
81
158
  export type RoutedTransferFrame = {
82
159
  kind: 'offer';
83
160
  header: FileTransferHeader;
161
+ } | {
162
+ kind: 'capabilities-request';
163
+ transferId: string;
164
+ header?: FileTransferHeader;
165
+ } | {
166
+ kind: 'capabilities';
167
+ transferId: string;
168
+ capabilities: TransferCapabilities;
84
169
  } | {
85
170
  kind: 'resume';
86
171
  transferId: string;
@@ -95,6 +180,11 @@ export type RoutedTransferFrame = {
95
180
  kind: 'ack';
96
181
  transferId: string;
97
182
  offset: number;
183
+ } | {
184
+ kind: 'waiting';
185
+ transferId: string;
186
+ offset: number;
187
+ leaseMs: number;
98
188
  } | {
99
189
  kind: 'complete';
100
190
  transferId: string;
@@ -117,6 +207,7 @@ export interface TransferRouteOptions<Result = ReceivedFile> extends TransferOpt
117
207
  export declare function encodeFrame(frame: RoutedTransferFrame, maxHeaderBytes?: number, maxChunkBytes?: number): Uint8Array;
118
208
  export declare function decodeFrame(input: Uint8Array, maxFileBytes?: number, maxHeaderBytes?: number, maxChunkBytes?: number): RoutedTransferFrame | null;
119
209
  export declare function createTransfer<Result = ReceivedFile>(client: TransferClient, options?: TransferRouteOptions<Result>): {
210
+ sendAccepted(deviceId: string, source: AcceptedFileSource, sendOptions?: AcceptedSendOptions): Promise<string>;
120
211
  send(deviceId: string, file: File, sendOptions?: SendFileOptions): Promise<string>;
121
212
  onIncoming(configured: ReceiveFileOptions<Result>): () => void;
122
213
  dispose(): void;
@@ -128,6 +219,7 @@ export declare function createTransfer<Result = ReceivedFile>(client: TransferCl
128
219
  * the consumer remains responsible for persisting the surrounding intent.
129
220
  */
130
221
  export declare function createRoutedTransfer<Result = ReceivedFile>(client: TransferClient, options?: TransferRouteOptions<Result>): {
222
+ sendAccepted(deviceId: string, source: AcceptedFileSource, sendOptions?: AcceptedSendOptions): Promise<string>;
131
223
  send(deviceId: string, file: File, sendOptions?: SendFileOptions): Promise<string>;
132
224
  onIncoming(configured: ReceiveFileOptions<Result>): () => void;
133
225
  dispose(): void;