@shenora/react 0.11.0 → 0.12.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.
@@ -3,11 +3,8 @@ import type { ShenoraEventBus } from './eventBus.js';
3
3
  import { type ShenoraStore } from './store.js';
4
4
  import type { IpcError } from './types.js';
5
5
  /**
6
- * Mirrors `Shenora.Core.Ipc.IpcRequestState` (design §4.2) — crosses the wire as its camelCase name for
7
- * free: `IpcJson` already installs a camelCase `JsonStringEnumConverter`, so no per-type wiring is
8
- * needed on either side. Pinned against the host by
9
- * `WireMirrorTests.Every_request_state_exists_on_both_sides` — a status added on one side and
10
- * not the other fails that test by name, not by a green suite that never looked.
6
+ * Mirrors `Shenora.Core.Ipc.IpcRequestState` (design §4.2), pinned against the host by
7
+ * `WireMirrorTests.Every_request_state_exists_on_both_sides`.
11
8
  */
12
9
  export declare const IpcRequestStates: {
13
10
  readonly Running: "running";
@@ -18,26 +15,21 @@ export declare const IpcRequestStates: {
18
15
  /** One of {@link IpcRequestStates}. */
19
16
  export type IpcRequestState = (typeof IpcRequestStates)[keyof typeof IpcRequestStates];
20
17
  /**
21
- * Mirrors `Shenora.Core.Ipc.IpcRequestEvents` — pinned against the host by
22
- * `WireMirrorTests.Request_event_names_match_the_host` (ALSO IN THIS BATCH, whole-branch review):
23
- * these were bare string literals with nothing comparing them to the host's own constants, so a host
24
- * rename left the suite green and the client permanently deaf to the renamed event.
18
+ * Mirrors `Shenora.Core.Ipc.IpcRequestEvents`, pinned against the host by
19
+ * `WireMirrorTests.Request_event_names_match_the_host`.
25
20
  */
26
21
  export declare const IpcRequestEventTypes: {
27
22
  readonly Updated: "REQUEST_UPDATED";
28
23
  /**
29
24
  * One or more request ids left the host with no corresponding `Updated` snapshot — history eviction
30
25
  * and `CLEAR_FINISHED`. Payload is `{ requestIds: string[] }`; {@link createRequestsStore} folds it
31
- * by deleting those ids. One authoritative event, so a client never has to guess what the host
32
- * removed.
26
+ * by deleting those ids.
33
27
  */
34
28
  readonly Removed: "REQUEST_REMOVED";
35
29
  };
36
30
  /**
37
- * Mirrors the route names `Shenora.Modules.Requests.IpcRequestsModule` switches on (its own
38
- * `ListType`/`CancelType`/`ClearFinishedType`/`ResumeType` constants) — pinned by
39
- * `WireMirrorTests.Request_route_names_match_the_hosts_module`, same rationale as
40
- * {@link IpcRequestEventTypes}.
31
+ * Mirrors the route names `Shenora.Modules.Requests.IpcRequestsModule` switches on, pinned by
32
+ * `WireMirrorTests.Request_route_names_match_the_hosts_module`.
41
33
  */
42
34
  export declare const IpcRequestRoutes: {
43
35
  readonly List: "LIST";
@@ -51,7 +43,7 @@ export declare const IpcRequestRoutes: {
51
43
  export declare const IpcRequestsModuleName = "SHENORA.REQUESTS";
52
44
  /**
53
45
  * Mirrors `Shenora.Core.Ipc.IpcLabel` — human-facing text the HOST never renders itself: an
54
- * untranslated fallback plus an app i18n key and interpolation parameters (headless, D13).
46
+ * untranslated fallback plus an app i18n key and interpolation parameters.
55
47
  */
56
48
  export interface IpcLabel {
57
49
  text?: string;
@@ -60,13 +52,11 @@ export interface IpcLabel {
60
52
  }
61
53
  /**
62
54
  * Mirrors `Shenora.Core.Ipc.IpcProgress` — how far a tracked operation has gotten, in the APP's own
63
- * unit, never a kit-assumed percent (generic-library audit, before publish: percent is not the
64
- * mechanism, it is one way an app happens to measure). `total` is the denominator when one is known;
65
- * `undefined` means there is NO known total — an absolute count with nothing to divide by (bytes
66
- * streamed so far off a chunked response, say), never zero. `unit` is app-defined, like `kind`
67
- * (`'bytes'`, `'files'`, `'percent'`) — the kit never interprets it and ships no percent helper: render
68
- * a ratio only when `total` is set, e.g. `total ? (value / total) * 100 : undefined` — that division
69
- * is the consumer's own policy (see the README example).
55
+ * unit rather than a kit-assumed percent. `unit` is app-defined (`'bytes'`, `'files'`, `'percent'`)
56
+ * and the kit never interprets it.
57
+ *
58
+ * ⚠ `total` undefined means there is NO known total — an absolute count with nothing to divide by,
59
+ * never zero. Render a ratio only when it is set; the README has the example.
70
60
  */
71
61
  export interface IpcProgress {
72
62
  value: number;
@@ -91,19 +81,11 @@ export interface IpcRequestStatus {
91
81
  finishedAt?: string;
92
82
  }
93
83
  /**
94
- * State behind {@link useShenoraRequests}. TWO bands, because a request is in flight or done:
95
- *
96
- * | Band | Getter |
97
- * |---|---|
98
- * | In flight | {@link running} |
99
- * | Finished | {@link finished} — prunable via `clearFinished` |
100
- *
101
- * Every getter is DERIVED from `byId` on each read — never a second copy a fold has to remember to
102
- * keep in sync. `byId` is the only thing any reducer here writes.
84
+ * State behind {@link useShenoraRequests}. `byId` is the only thing a reducer writes; the two bands
85
+ * are derived from it on every read.
103
86
  *
104
87
  * ⚠ Most requests never appear at all: one that finishes inside the host's grace period is never
105
- * announced, so this store is a list of work that is actually TAKING A WHILE rather than a log of
106
- * every call the page made.
88
+ * announced, so this lists work that is actually TAKING A WHILE, not every call the page made.
107
89
  */
108
90
  export interface RequestsState {
109
91
  byId: Record<string, IpcRequestStatus>;
@@ -118,11 +100,9 @@ export interface RequestsActions {
118
100
  cancel: (requestId: string) => string;
119
101
  /**
120
102
  * `CLEAR_FINISHED { scope? }` — drop retained finished history, forwarding this store's own
121
- * configured scope (generic-library audit finding 1) so a scoped store's "clear completed" cannot
122
- * wipe another scope's history host-side. No local mutation here: the host's
123
- * `REQUEST_REMOVED` (finding 4) is the only thing that removes a row from this store now — see
124
- * {@link IpcRequestEventTypes.Removed}. It used to carry an optimistic local prune of every TERMINAL
125
- * entry, added because removals had no wire event at all; that guess is retired now that one exists.
103
+ * configured scope so a scoped store's "clear completed" cannot wipe another scope's history
104
+ * host-side. Nothing is mutated locally: the host's {@link IpcRequestEventTypes.Removed} is the only
105
+ * thing that removes a row.
126
106
  */
127
107
  clearFinished: () => string;
128
108
  }
@@ -130,19 +110,17 @@ export interface RequestsActions {
130
110
  export interface RequestsStoreOptions {
131
111
  /**
132
112
  * The request/event module this store talks to. Must match the host's
133
- * `IpcRequestTrackerOptions.ModuleName` — default `'SHENORA.REQUESTS'` on both sides — when an app
134
- * renamed it to avoid a collision with one of its own module names (the duplicate-module guard
135
- * `IpcRequestsModule`'s own docs describe). A store bound to the default name cannot reach a
136
- * renamed host at all, which is exactly the gap this field closes.
113
+ * `IpcRequestTrackerOptions.ModuleName` — default `'SHENORA.REQUESTS'` on both sides — for an app
114
+ * that renamed it to avoid colliding with one of its own modules.
137
115
  */
138
116
  module?: string;
139
117
  /**
140
- * Optional app-defined scope, applied to THREE places so the store stays internally consistent:
141
- * the bus subscription (only deltas whose event scope matches are folded), the actions' request
142
- * envelope, and the initial `LIST` snapshot's payload (`IpcRequestsModule` reads its scope filter
143
- * from the payload, not the envelope — see `IpcRequestsModule.RouteMessageAsync`). Threading it
144
- * into only the first two would load every scope on first subscribe and never remove the
145
- * out-of-scope rows, since no delta for them ever arrives: a silent, permanent leak.
118
+ * Optional app-defined scope, applied to THREE places: the bus subscription, the actions' request
119
+ * envelope, and the initial `LIST` snapshot's PAYLOAD (`IpcRequestsModule` reads its scope filter
120
+ * from the payload, not the envelope).
121
+ *
122
+ * ⚠ All three or none. Threading it into only the first two loads every scope on first subscribe and
123
+ * then never sheds the out-of-scope rows, since no delta for them ever arrives.
146
124
  */
147
125
  scope?: string;
148
126
  /** Test/multi-transport seams. Default: the shared bridge and event bus. */
@@ -150,27 +128,18 @@ export interface RequestsStoreOptions {
150
128
  bus?: ShenoraEventBus;
151
129
  }
152
130
  /**
153
- * Build a store instance over the requests module — the factory {@link useShenoraRequests}
154
- * itself is built from. Exposed (rather than only the ready-made hook) for the same reason
155
- * `WindowCommands` takes an optional bridge: a test needs a fake bridge/bus
156
- * (`requests.test.ts`), an app that renamed the host's `IpcRequestTrackerOptions.ModuleName`
157
- * needs a store bound to that name instead of the unreachable default, and an app running a
158
- * secondary window or auxiliary session needs its own scope-filtered instance instead of being
159
- * stuck with the shared, unscoped default.
131
+ * Build a store instance over the requests module — the factory {@link useShenoraRequests} is built
132
+ * from. Use it directly for a renamed host module, a scope-filtered instance, or a fake bridge/bus.
160
133
  */
161
134
  export declare function createRequestsStore(options?: RequestsStoreOptions): ShenoraStore<RequestsState, RequestsActions>;
162
135
  /**
163
136
  * The client side of the operations primitive (design §4.6, `IpcRequestsModule` +
164
137
  * `IIpcRequestTracker`): snapshots via `LIST` on first subscribe, then folds `REQUEST_UPDATED` by
165
- * id — one subscription however many components read it, and a late mounter renders CURRENT state
166
- * because the host is authoritative (the store primitive's own late-mounter case is now
167
- * host-backed end to end). `running`/`finished` are selectors an activity panel or status bar reads
168
- * directly: `useShenoraRequests((s) => s.running)`. There is no `waiting` band and no `waitReason` —
169
- * a request is IN FLIGHT or DONE, the same two states `XMLHttpRequest` has, since D66. Bound to the
170
- * default module/no scope — use {@link createRequestsStore} directly for a renamed module or a
171
- * scope-filtered instance.
138
+ * id — one subscription however many components read it, and a late mounter renders CURRENT state.
139
+ * `running`/`finished` are selectors an activity panel reads directly:
140
+ * `useShenoraRequests((s) => s.running)`.
172
141
  *
173
- * Headless, per D13: no component, no UI opinion, no `ProcessType`-style enum — what an operation
174
- * IS stays the app's `kind` string; this only carries the uniform lifecycle around it.
142
+ * Bound to the default module and no scope — use {@link createRequestsStore} for a renamed module or
143
+ * a scope-filtered instance.
175
144
  */
176
145
  export declare const useShenoraRequests: ShenoraStore<RequestsState, RequestsActions>;
package/dist/requests.js CHANGED
@@ -1,48 +1,35 @@
1
1
  import { createShenoraStore } from './store.js';
2
2
  /**
3
- * Mirrors `Shenora.Core.Ipc.IpcRequestState` (design §4.2) — crosses the wire as its camelCase name for
4
- * free: `IpcJson` already installs a camelCase `JsonStringEnumConverter`, so no per-type wiring is
5
- * needed on either side. Pinned against the host by
6
- * `WireMirrorTests.Every_request_state_exists_on_both_sides` — a status added on one side and
7
- * not the other fails that test by name, not by a green suite that never looked.
3
+ * Mirrors `Shenora.Core.Ipc.IpcRequestState` (design §4.2), pinned against the host by
4
+ * `WireMirrorTests.Every_request_state_exists_on_both_sides`.
8
5
  */
9
6
  export const IpcRequestStates = {
10
7
  Running: 'running',
11
8
  Completed: 'completed',
12
9
  Failed: 'failed',
13
10
  Cancelled: 'cancelled',
14
- // NO 'waiting'. A request is IN FLIGHT or DONE — the XHR model this mirrors has no parked state,
15
- // and neither does the host since D66. Work that parks awaiting a human is host-initiated work
16
- // (a queued mission), which reports on its own event stream rather than as a request.
17
11
  };
18
12
  /**
19
- * Mirrors `Shenora.Core.Ipc.IpcRequestEvents` — pinned against the host by
20
- * `WireMirrorTests.Request_event_names_match_the_host` (ALSO IN THIS BATCH, whole-branch review):
21
- * these were bare string literals with nothing comparing them to the host's own constants, so a host
22
- * rename left the suite green and the client permanently deaf to the renamed event.
13
+ * Mirrors `Shenora.Core.Ipc.IpcRequestEvents`, pinned against the host by
14
+ * `WireMirrorTests.Request_event_names_match_the_host`.
23
15
  */
24
16
  export const IpcRequestEventTypes = {
25
17
  Updated: 'REQUEST_UPDATED',
26
18
  /**
27
19
  * One or more request ids left the host with no corresponding `Updated` snapshot — history eviction
28
20
  * and `CLEAR_FINISHED`. Payload is `{ requestIds: string[] }`; {@link createRequestsStore} folds it
29
- * by deleting those ids. One authoritative event, so a client never has to guess what the host
30
- * removed.
21
+ * by deleting those ids.
31
22
  */
32
23
  Removed: 'REQUEST_REMOVED',
33
24
  };
34
25
  /**
35
- * Mirrors the route names `Shenora.Modules.Requests.IpcRequestsModule` switches on (its own
36
- * `ListType`/`CancelType`/`ClearFinishedType`/`ResumeType` constants) — pinned by
37
- * `WireMirrorTests.Request_route_names_match_the_hosts_module`, same rationale as
38
- * {@link IpcRequestEventTypes}.
26
+ * Mirrors the route names `Shenora.Modules.Requests.IpcRequestsModule` switches on, pinned by
27
+ * `WireMirrorTests.Request_route_names_match_the_hosts_module`.
39
28
  */
40
29
  export const IpcRequestRoutes = {
41
30
  List: 'LIST',
42
31
  Cancel: 'CANCEL',
43
32
  ClearFinished: 'CLEAR_FINISHED',
44
- // THREE routes — the same three `XMLHttpRequest` offers. RESUME/WAIT/DISMISS went with the waiting
45
- // band (D66), and the wire-mirror test pins this object's SIZE so a retired name cannot creep back.
46
33
  };
47
34
  /**
48
35
  * Default `Shenora.Core.Ipc.IpcRequestTrackerOptions.ModuleName` — pinned by
@@ -61,9 +48,7 @@ function index(list) {
61
48
  byId[operation.id] = operation;
62
49
  return byId;
63
50
  }
64
- /**
65
- * The one place `running`/`finished` are computed — wrap `byId` here, nowhere else.
66
- */
51
+ /** The one place `running`/`finished` are computed — wrap `byId` here, nowhere else. */
67
52
  function makeState(byId) {
68
53
  return {
69
54
  byId,
@@ -76,23 +61,16 @@ function makeState(byId) {
76
61
  };
77
62
  }
78
63
  /**
79
- * Build a store instance over the requests module — the factory {@link useShenoraRequests}
80
- * itself is built from. Exposed (rather than only the ready-made hook) for the same reason
81
- * `WindowCommands` takes an optional bridge: a test needs a fake bridge/bus
82
- * (`requests.test.ts`), an app that renamed the host's `IpcRequestTrackerOptions.ModuleName`
83
- * needs a store bound to that name instead of the unreachable default, and an app running a
84
- * secondary window or auxiliary session needs its own scope-filtered instance instead of being
85
- * stuck with the shared, unscoped default.
64
+ * Build a store instance over the requests module — the factory {@link useShenoraRequests} is built
65
+ * from. Use it directly for a renamed host module, a scope-filtered instance, or a fake bridge/bus.
86
66
  */
87
67
  export function createRequestsStore(options = {}) {
88
68
  const module = options.module ?? IpcRequestsModuleName;
89
69
  return createShenoraStore(module, {
90
70
  initial: makeState({}),
91
- // LIST is the snapshot source (design §4.6): a store cannot replay a stream, so a component
92
- // that mounts while work is already running gets it from here before folding any deltas.
93
- // The payload carries `scope` so the initial load is filtered the SAME way the deltas are
94
- // (below, and via createShenoraStore's own `scope` option) — both halves must agree, or a
95
- // scoped store loads every scope once and then never sheds the out-of-scope rows.
71
+ // LIST is the snapshot source (design §4.6): a store cannot replay a stream, so a component that
72
+ // mounts while work is already running gets it from here before folding any deltas. The payload
73
+ // carries `scope` so the initial load is filtered the same way the deltas are.
96
74
  snapshot: {
97
75
  type: IpcRequestRoutes.List,
98
76
  payload: options.scope !== undefined ? { scope: options.scope } : undefined,
@@ -102,9 +80,8 @@ export function createRequestsStore(options = {}) {
102
80
  // ONE event type for every transition (design §4.3) — last-write-wins by id, so folding needs
103
81
  // no ordering logic and no cross-type races.
104
82
  [IpcRequestEventTypes.Updated]: (state, payload) => makeState({ ...state.byId, [payload.id]: payload }),
105
- // The ONE removal delta (Finding 4, generic-library audit), replacing the two hand-written
106
- // optimistic prunes `clearFinished`/`resume` used to carry (see their own docs below) — deletes
107
- // exactly the ids the host named, regardless of status; an id this store never had is a no-op.
83
+ // The ONE removal delta: deletes exactly the ids the host named, regardless of status. An id
84
+ // this store never had is a no-op.
108
85
  [IpcRequestEventTypes.Removed]: (state, payload) => {
109
86
  const byId = { ...state.byId };
110
87
  for (const id of payload.requestIds)
@@ -114,13 +91,7 @@ export function createRequestsStore(options = {}) {
114
91
  },
115
92
  actions: ({ post }) => ({
116
93
  cancel: (requestId) => post(IpcRequestRoutes.Cancel, { payload: { requestId } }),
117
- clearFinished: () =>
118
- // Forward THIS store's own configured scope (Finding 1, generic-library audit) — the same
119
- // key the LIST snapshot payload already carries above. No local mutation here any more
120
- // (Finding 4): the host's REQUEST_REMOVED is the ONLY thing that removes a row now, which
121
- // is also what makes the scope threading safe to add — nothing here can diverge from what
122
- // the host actually cleared.
123
- post(IpcRequestRoutes.ClearFinished, {
94
+ clearFinished: () => post(IpcRequestRoutes.ClearFinished, {
124
95
  payload: options.scope !== undefined ? { scope: options.scope } : undefined,
125
96
  }),
126
97
  }),
@@ -132,15 +103,11 @@ export function createRequestsStore(options = {}) {
132
103
  /**
133
104
  * The client side of the operations primitive (design §4.6, `IpcRequestsModule` +
134
105
  * `IIpcRequestTracker`): snapshots via `LIST` on first subscribe, then folds `REQUEST_UPDATED` by
135
- * id — one subscription however many components read it, and a late mounter renders CURRENT state
136
- * because the host is authoritative (the store primitive's own late-mounter case is now
137
- * host-backed end to end). `running`/`finished` are selectors an activity panel or status bar reads
138
- * directly: `useShenoraRequests((s) => s.running)`. There is no `waiting` band and no `waitReason` —
139
- * a request is IN FLIGHT or DONE, the same two states `XMLHttpRequest` has, since D66. Bound to the
140
- * default module/no scope — use {@link createRequestsStore} directly for a renamed module or a
141
- * scope-filtered instance.
106
+ * id — one subscription however many components read it, and a late mounter renders CURRENT state.
107
+ * `running`/`finished` are selectors an activity panel reads directly:
108
+ * `useShenoraRequests((s) => s.running)`.
142
109
  *
143
- * Headless, per D13: no component, no UI opinion, no `ProcessType`-style enum — what an operation
144
- * IS stays the app's `kind` string; this only carries the uniform lifecycle around it.
110
+ * Bound to the default module and no scope — use {@link createRequestsStore} for a renamed module or
111
+ * a scope-filtered instance.
145
112
  */
146
113
  export const useShenoraRequests = createRequestsStore();
@@ -3,24 +3,20 @@ import { type MediaSourceGlobals } from './segmentStream.js';
3
3
  * The imperative half of the segment route (D71 piece 4b): open a `SourceBuffer`, feed it, and stop when
4
4
  * the platform says stop.
5
5
  *
6
- * 🔴 **Everything here was written against measurements rather than against the specification**, because
7
- * three implementations disagree in ways the spec permits and none of it is guessable:
6
+ * 🔴 **Three implementations disagree in ways the spec permits, so none of this is guessable** — the
7
+ * measurements are in `docs/design/media.md`:
8
8
  *
9
- * - **Attachment is not portable.** iOS takes `srcObject` — a `ManagedMediaSource` is a valid
10
- * `MediaSourceHandle` — and Chromium refuses it outright ("not of type '(MediaSourceHandle or
11
- * MediaStream)'"), wanting an object URL. Feature-detected, not branched on the shell: which one works
12
- * is a property of the MediaSource, not of the OS.
9
+ * - **Attachment is not portable.** iOS takes `srcObject` (a `ManagedMediaSource` is a valid
10
+ * `MediaSourceHandle`); Chromium refuses it and wants an object URL. Feature-detected, not branched on
11
+ * the shell: which one works is a property of the MediaSource, not of the OS.
13
12
  * - **The codecs are read from the init segment, never assumed.** The track set is a fact about the
14
13
  * DEVICE, not the source: the same file yields a two-track init on iOS and a video-only one on Android,
15
14
  * which cannot decode its AC-3 soundtrack. A mismatch kills the FIRST append and plays nothing.
16
- * - **The streaming gate is real on iOS and absent elsewhere.** `endstreaming` fires once enough is
17
- * buffered (measured: at 60 s, not at 6 s), and fetching past it is the misuse `ManagedMediaSource`
18
- * exists to detect. A plain `MediaSource` has neither event nor a `streaming` property, and its absence
19
- * means "always streaming" — never "never asked".
15
+ * - **The streaming gate is real on iOS and absent elsewhere.** Fetching past `endstreaming` is the
16
+ * misuse `ManagedMediaSource` exists to detect. A plain `MediaSource` has neither the event nor a
17
+ * `streaming` property, and its absence means "always streaming" — never "never asked".
20
18
  *
21
- * ⚠ **The dependencies are INJECTABLE so this is testable without a browser.** jsdom has no MediaSource,
22
- * and "cannot be verified anywhere this repo runs" was true of the whole file until the seams below
23
- * existed. A fake source and a fake fetch drive every branch here.
19
+ * The dependencies below are injectable, so a fake source and a fake fetch drive every branch here.
24
20
  */
25
21
  export interface SegmentBinderOptions {
26
22
  /** The playlist URL. Segment URIs are resolved relative to it. */
@@ -38,11 +34,7 @@ export interface SegmentBinderOptions {
38
34
  }>;
39
35
  /** Defaults to `URL.createObjectURL`. Only used when `srcObject` is refused. */
40
36
  createObjectURL?: (source: object) => string;
41
- /**
42
- * Defaults to `URL.revokeObjectURL`. The pair to {@link createObjectURL}, and injectable for the same
43
- * reason: without it the failure paths that must revoke — a source that closes before opening, a
44
- * codec the device refuses — cannot be asserted, only hoped for.
45
- */
37
+ /** Defaults to `URL.revokeObjectURL`. The pair to {@link createObjectURL}. */
46
38
  revokeObjectURL?: (url: string) => void;
47
39
  /** Stop fetching once this many seconds are buffered ahead. Defaults to 30. */
48
40
  targetAheadSeconds?: number;
@@ -65,9 +57,8 @@ export interface SegmentBinding {
65
57
  /**
66
58
  * Thrown for every reason a stream cannot start, so a caller has one thing to catch.
67
59
  *
68
- * ⚠ **Not literally every reason** — a `TypeError` from the manifest fetch failing at the network layer,
69
- * and a `RangeError` from a truncated init segment, both propagate as themselves. Catch broadly if you
70
- * need to be exhaustive.
60
+ * ⚠ **Not literally every reason** — a `TypeError` from the manifest fetch and a `RangeError` from a
61
+ * truncated init segment propagate as themselves. Catch broadly if you need to be exhaustive.
71
62
  */
72
63
  export declare class SegmentBinderError extends Error {
73
64
  constructor(message: string);
@@ -78,10 +69,6 @@ export declare class SegmentBinderError extends Error {
78
69
  * Resolves once the init segment has been appended — the point after which the element can play — and
79
70
  * goes on fetching in the background until every segment is in or {@link SegmentBinding.dispose} is called.
80
71
  *
81
- * ⚠ **There is deliberately NO `useSegmentStream` hook.** This needs no React — it takes an element and
82
- * returns a handle, exactly as `mediaUrl` takes a path and returns a string — so a hook would add a
83
- * lifecycle without adding a capability. The case that would earn one is an app wanting load/error
84
- * state as component state, and the shape of that hook depends on what such an app actually asks for;
85
- * inventing it first is how a seam nothing consults gets built (D63). Call this from an effect.
72
+ * There is no `useSegmentStream` hook: this needs no React, so call it from an effect.
86
73
  */
87
74
  export declare function bindSegmentStream(options: SegmentBinderOptions): Promise<SegmentBinding>;
@@ -1,19 +1,15 @@
1
1
  import { codecsFromInitSegment, nextSegment, parseManifest, pickMediaSource, segmentMimeType, } from './segmentStream.js';
2
2
  /**
3
- * How long to wait for the attached MediaSource to reach `open`.
4
- *
5
- * Attachment is local and immediate — there is no network in it — so this is a deadline for "something
6
- * is wrong", not a budget. It exists because the alternative to a deadline here is an await that never
7
- * returns: the element can refuse or tear down an attachment without ever raising an event this side
8
- * can name.
3
+ * How long to wait for the attached MediaSource to reach `open`. Attachment is local and immediate, so
4
+ * this is a deadline for "something is wrong": the element can refuse or tear down an attachment
5
+ * without raising any event this side can name, and the await would never return.
9
6
  */
10
7
  const ATTACH_TIMEOUT_MS = 10000;
11
8
  /**
12
9
  * Thrown for every reason a stream cannot start, so a caller has one thing to catch.
13
10
  *
14
- * ⚠ **Not literally every reason** — a `TypeError` from the manifest fetch failing at the network layer,
15
- * and a `RangeError` from a truncated init segment, both propagate as themselves. Catch broadly if you
16
- * need to be exhaustive.
11
+ * ⚠ **Not literally every reason** — a `TypeError` from the manifest fetch and a `RangeError` from a
12
+ * truncated init segment propagate as themselves. Catch broadly if you need to be exhaustive.
17
13
  */
18
14
  export class SegmentBinderError extends Error {
19
15
  constructor(message) {
@@ -42,27 +38,17 @@ function bufferedAhead(element) {
42
38
  * Resolves once the init segment has been appended — the point after which the element can play — and
43
39
  * goes on fetching in the background until every segment is in or {@link SegmentBinding.dispose} is called.
44
40
  *
45
- * ⚠ **There is deliberately NO `useSegmentStream` hook.** This needs no React — it takes an element and
46
- * returns a handle, exactly as `mediaUrl` takes a path and returns a string — so a hook would add a
47
- * lifecycle without adding a capability. The case that would earn one is an app wanting load/error
48
- * state as component state, and the shape of that hook depends on what such an app actually asks for;
49
- * inventing it first is how a seam nothing consults gets built (D63). Call this from an effect.
41
+ * There is no `useSegmentStream` hook: this needs no React, so call it from an effect.
50
42
  */
51
43
  export async function bindSegmentStream(options) {
52
44
  const { manifest: manifestUrl, element, globals = globalThis, targetAheadSeconds = 30, onDiagnostic, } = options;
53
45
  const doFetch = options.fetch ?? ((url) => fetch(url));
54
46
  const say = (line) => onDiagnostic?.(line);
55
47
  /**
56
- * A FAILURE, as opposed to a trace line.
57
- *
58
- * 🔴 **`onDiagnostic` is optional, so failures used to go nowhere by default.** A segment answering 500
59
- * or an `appendBuffer` `QuotaExceededError` produced no console output, no rejection and no state
60
- * change — playback simply stalled, with nothing anywhere to explain it. Every other error path in this
61
- * package already defaults to `console.error` (`bridge.ts`'s `onPostError`, `store.ts`'s `onError`,
62
- * `eventBus.ts`); this one was the exception.
63
- *
64
- * ⚠ Only when the app supplied NO handler. A caller that took `onDiagnostic` owns its reporting and
65
- * must not be double-logged — the same rule the two sinks above follow.
48
+ * A FAILURE, as opposed to a trace line: reported to `console.error` when the app supplied no
49
+ * `onDiagnostic`, so a segment answering 500 or an `appendBuffer` `QuotaExceededError` cannot stall
50
+ * playback with nothing anywhere to explain it. A caller that took `onDiagnostic` owns its reporting
51
+ * and is not double-logged.
66
52
  */
67
53
  const fail = (line) => (onDiagnostic ? onDiagnostic(line) : console.error(`[shenora] ${line}`));
68
54
  const kind = pickMediaSource(globals);
@@ -105,19 +91,18 @@ export async function bindSegmentStream(options) {
105
91
  element.src = objectUrl;
106
92
  }
107
93
  // ⚠ The object URL is minted BEFORE anything below can fail, so EVERY failure between the mint and
108
- // the returned binding has to revoke it itself — bindSegmentStream has not returned, so the caller
109
- // holds no binding to dispose, and the document keeps the MediaSource alive for its lifetime. That
110
- // is three places, not one: the open wait, addSourceBuffer, and the init append.
94
+ // the returned binding must revoke it itself — the caller holds no binding to dispose yet, and the
95
+ // document keeps the MediaSource alive for its lifetime. Three places: the open wait,
96
+ // addSourceBuffer, and the init append.
111
97
  const revokeAndRethrow = (e) => {
112
98
  if (objectUrl)
113
99
  revoke(objectUrl);
114
100
  throw e;
115
101
  };
116
- // 🔴 `error` IS NOT A MediaSource EVENT. The spec fires `sourceopen`, `sourceended` and
117
- // `sourceclose`, so the only rejection path here could never fire — and an attachment that CLOSES
118
- // rather than opening (the element detached from the document before load, an attachment refused)
119
- // left this await pending FOREVER: no error, no diagnostic, no way to reach dispose(), and the object
120
- // URL never revoked. `sourceclose` is the real signal, and the deadline covers whatever is neither.
102
+ // 🔴 `error` IS NOT A MediaSource EVENT — the spec fires `sourceopen`, `sourceended` and
103
+ // `sourceclose`. An attachment that CLOSES rather than opening (the element detached before load, an
104
+ // attachment refused) leaves this await pending FOREVER on any other listener: no error, no
105
+ // diagnostic, no way to reach dispose(). The deadline covers whatever is neither.
121
106
  await new Promise((res, rej) => {
122
107
  if (source.readyState === 'open')
123
108
  return res();
@@ -135,8 +120,7 @@ export async function bindSegmentStream(options) {
135
120
  }).catch(revokeAndRethrow);
136
121
  let buffer;
137
122
  try {
138
- // Throws synchronously for a codecs string this implementation refuses — a real case, per the
139
- // remarks on codec derivation above.
123
+ // Throws synchronously for a codecs string this implementation refuses.
140
124
  buffer = source.addSourceBuffer(mime);
141
125
  }
142
126
  catch (e) {
@@ -152,9 +136,8 @@ export async function bindSegmentStream(options) {
152
136
  * Append one buffer and settle when the source buffer says so.
153
137
  *
154
138
  * 🔴 **Both listeners come off on EITHER outcome.** `{ once: true }` removes only the listener that
155
- * FIRES, and the success path fires `updateend` — so the `error` listener stayed attached, once per
156
- * appended segment, each retaining a settled `rej` closure. A two-hour stream at six-second segments
157
- * accumulates ~1,200 dead listeners on one `SourceBuffer`, none of which `dispose()` could shed, and a
139
+ * FIRES, and the success path fires `updateend` — so with it the `error` listener stays attached once
140
+ * per appended segment, each retaining a settled `rej` closure that `dispose()` cannot shed, and one
158
141
  * later real `error` invokes every one of them.
159
142
  */
160
143
  const append = (bytes) => new Promise((res, rej) => {
@@ -182,9 +165,9 @@ export async function bindSegmentStream(options) {
182
165
  /**
183
166
  * Fetch and append whatever {@link nextSegment} asks for, one at a time.
184
167
  *
185
- * ⚠ Re-entrancy is guarded rather than queued: this is driven by element events that fire far faster
186
- * than an append completes, and two concurrent `appendBuffer` calls on one SourceBuffer throw
187
- * `InvalidStateError` — which surfaces as a stall with no obvious cause.
168
+ * ⚠ Re-entrancy is guarded rather than queued: element events fire far faster than an append
169
+ * completes, and two concurrent `appendBuffer` calls on one SourceBuffer throw `InvalidStateError`,
170
+ * which surfaces as a stall with no obvious cause.
188
171
  */
189
172
  const pump = async () => {
190
173
  if (pumping || disposed)
@@ -202,8 +185,8 @@ export async function bindSegmentStream(options) {
202
185
  if (disposed)
203
186
  return;
204
187
  if (!response.ok) {
205
- // 503 is the host saying "still producing", which is a WAIT and not a failure — the route
206
- // answers it deliberately rather than 404ing a source that is merely not ready.
188
+ // 503 is the host saying "still producing" — a WAIT, not a failure. The route answers it
189
+ // rather than 404ing a source that is merely not ready yet.
207
190
  fail(`segments: ${entry.uri} answered ${response.status}`);
208
191
  break;
209
192
  }