lanka 1.0.1 → 1.1.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.
- package/README.md +5 -3
- package/dist/{ALankaGateway-ExlRGT3D.d.ts → ALankaGateway-CkW1LbKE.d.ts} +50 -4
- package/dist/{ILankaScenarioMetadata-Bu-yggTZ.d.ts → ILankaScenarioMetadata-GoWWNEQL.d.ts} +1 -1
- package/dist/{ILankaScenarioVM-DuCyPoyT.d.ts → ILankaScenarioVM-DUsI-fSc.d.ts} +116 -3
- package/dist/{LankaScenarioLocator-CLkq4MaJ.d.ts → LankaScenarioLocator-D86TIwiu.d.ts} +10 -5
- package/dist/_extend/index.d.ts +4 -4
- package/dist/_extend/index.js +2 -2
- package/dist/_internal/index.d.ts +5 -5
- package/dist/{activeRuntime-DT4gB16d.d.ts → activeRuntime-B336NU5I.d.ts} +2 -2
- package/dist/bootstrap/index.d.ts +13 -144
- package/dist/bootstrap/index.js +4 -4
- package/dist/{chunk-MYZQYOMD.js → chunk-5MAQVBI2.js} +20 -15
- package/dist/chunk-5MAQVBI2.js.map +1 -0
- package/dist/chunk-D5WKKEIR.js +54 -0
- package/dist/chunk-D5WKKEIR.js.map +1 -0
- package/dist/{chunk-63ST2UKP.js → chunk-LMKLLEHA.js} +103 -17
- package/dist/chunk-LMKLLEHA.js.map +1 -0
- package/dist/{chunk-YXI4OQEV.js → chunk-O5ROO7QF.js} +27 -4
- package/dist/chunk-O5ROO7QF.js.map +1 -0
- package/dist/{chunk-HZAIAGWS.js → chunk-UGXSGQPW.js} +6 -2
- package/dist/chunk-UGXSGQPW.js.map +1 -0
- package/dist/createLanka-DI1CSy2Q.d.ts +139 -0
- package/dist/gateway/index.d.ts +77 -67
- package/dist/gateway/index.js +50 -54
- package/dist/gateway/index.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.js +4 -4
- package/dist/locator/index.d.ts +1 -1
- package/dist/scenario/index.d.ts +6 -4
- package/dist/scenario/index.js +2 -2
- package/dist/stream/index.d.ts +386 -0
- package/dist/stream/index.js +287 -0
- package/dist/stream/index.js.map +1 -0
- package/dist/validation/index.js +4 -48
- package/dist/validation/index.js.map +1 -1
- package/dist/viewmodel/index.d.ts +1 -1
- package/dist/viewmodel/index.js +2 -2
- package/package.json +7 -3
- package/skills/lanka-core/SKILL.md +1 -1
- package/skills/lanka-core/reference.md +110 -17
- package/skills/lanka-packages/SKILL.md +1 -1
- package/dist/chunk-63ST2UKP.js.map +0 -1
- package/dist/chunk-HZAIAGWS.js.map +0 -1
- package/dist/chunk-MYZQYOMD.js.map +0 -1
- package/dist/chunk-YXI4OQEV.js.map +0 -1
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { c as ILankaPlugin } from '../createLanka-DI1CSy2Q.js';
|
|
2
|
+
import '../createLankaScope-BiFxNQgl.js';
|
|
3
|
+
import '../LankaSharedStoreLocator-zS2kLu-S.js';
|
|
4
|
+
import '../ALankaSharedStore-B7uepuuk.js';
|
|
5
|
+
import 'zustand/vanilla';
|
|
6
|
+
import '../activeRuntime-B336NU5I.js';
|
|
7
|
+
import '../ILankaScenarioVM-DUsI-fSc.js';
|
|
8
|
+
import '../LankaScenarioLocator-D86TIwiu.js';
|
|
9
|
+
import '../ILankaScenarioMetadata-GoWWNEQL.js';
|
|
10
|
+
import '../ALankaGateway-CkW1LbKE.js';
|
|
11
|
+
import '../lankaStandardValidator-CL-r-zEV.js';
|
|
12
|
+
import '@standard-schema/spec';
|
|
13
|
+
import '../ILankaRuntimeConfig-Vl436GWK.js';
|
|
14
|
+
import '../lankaHttpInFlight-Bk1eIuSx.js';
|
|
15
|
+
import '../lankaRequestMiddleware-DAC5kCb7.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* What a subscriber is handed when a named event arrives.
|
|
19
|
+
*
|
|
20
|
+
* A record rather than a generic: the transport parsed bytes and knows nothing
|
|
21
|
+
* about what they mean, and a bridge is the layer that names the shape. Typing
|
|
22
|
+
* it here would be the transport asserting something it did not check.
|
|
23
|
+
*/
|
|
24
|
+
type TLankaStreamEventCallback = (payload: Record<string, unknown>) => void;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Called after a connection is re-established, never on the first one.
|
|
28
|
+
*
|
|
29
|
+
* No arguments deliberately: the one fact it carries is "there is a gap in what
|
|
30
|
+
* you were told", and nothing about the gap is knowable from this side.
|
|
31
|
+
*/
|
|
32
|
+
type TLankaStreamReconnectCallback = () => void;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A connection that pushes named events, whatever carries them.
|
|
36
|
+
*
|
|
37
|
+
* The port is the whole reason a screen can stop caring which protocol it is
|
|
38
|
+
* on. `text/event-stream`, a WebSocket, a `graphql-ws` subscription and a gRPC
|
|
39
|
+
* server stream differ in how bytes arrive and in nothing a bridge can see —
|
|
40
|
+
* and the choice between them is made by a proxy, a load balancer or a backend
|
|
41
|
+
* team, none of which the application controls.
|
|
42
|
+
*
|
|
43
|
+
* What a bridge needs is exactly this: subscribe to a named event, learn that
|
|
44
|
+
* the connection came back, and be told whether there is a connection to be had
|
|
45
|
+
* at all. Everything else — reconnect backoff, envelope parsing, credentials,
|
|
46
|
+
* a handshake — is an implementation's own business, and `ALankaStreamTransport`
|
|
47
|
+
* is where the parts that are the same for all of them live.
|
|
48
|
+
*
|
|
49
|
+
* ## Kept minimal on purpose
|
|
50
|
+
*
|
|
51
|
+
* A CONSUMER implements this — a native bridge, a mock, a socket the
|
|
52
|
+
* application already had — so every member added later is a compile error in
|
|
53
|
+
* code nobody touched (`skills/surface/SKILL.md` §6b). Five members is not
|
|
54
|
+
* meanness; it is the only form the promise survives in.
|
|
55
|
+
*/
|
|
56
|
+
interface ILankaServerEventTransport {
|
|
57
|
+
/** Whether this engine can carry server events at all. */
|
|
58
|
+
isSupported: () => boolean;
|
|
59
|
+
connect: () => void;
|
|
60
|
+
disconnect: () => void;
|
|
61
|
+
/** Subscribes to one named event. Returns the unsubscribe. */
|
|
62
|
+
on: (eventType: string, callback: TLankaStreamEventCallback) => () => void;
|
|
63
|
+
/**
|
|
64
|
+
* Called after a connection is re-established, never on the first one.
|
|
65
|
+
*
|
|
66
|
+
* What it is for: everything that happened while the connection was down was
|
|
67
|
+
* missed, so a screen refetches rather than assuming it is current.
|
|
68
|
+
*/
|
|
69
|
+
onReconnect: (callback: TLankaStreamReconnectCallback) => () => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ILankaStreamTriggerContext {
|
|
73
|
+
/** Whether a SERVER event handler is running right now. */
|
|
74
|
+
isActive: () => boolean;
|
|
75
|
+
/** Runs a function marked as triggered by a server event. */
|
|
76
|
+
run: <T>(fn: () => T) => T;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The "this change came from outside" marker.
|
|
80
|
+
*
|
|
81
|
+
* A handler that updates state cannot tell its own change from someone else's:
|
|
82
|
+
* the user pressed a button and then receives the server event about that
|
|
83
|
+
* button. Without the marker the screen notifies the user about their own
|
|
84
|
+
* action, and an optimistic update is rolled back by a "foreign" response that
|
|
85
|
+
* in fact confirms it.
|
|
86
|
+
*
|
|
87
|
+
* ## An instance, not a module variable
|
|
88
|
+
*
|
|
89
|
+
* A module-level flag is one per process: two framework instances (a test beside
|
|
90
|
+
* the app) would share it, and one instance's handler would see a marker set by
|
|
91
|
+
* the other.
|
|
92
|
+
*
|
|
93
|
+
* ## Boundaries
|
|
94
|
+
*
|
|
95
|
+
* The marker is SYNCHRONOUS: it holds for the duration of the call and is
|
|
96
|
+
* cleared in `finally`. An async continuation (an `await` inside the handler)
|
|
97
|
+
* no longer sees it — honestly so: after an `await` control has been anywhere,
|
|
98
|
+
* and claiming "we are still inside a server event" would be untrue.
|
|
99
|
+
*/
|
|
100
|
+
declare const createLankaStreamTriggerContext: () => ILankaStreamTriggerContext;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A server-event -> application-scenario bridge.
|
|
104
|
+
*
|
|
105
|
+
* It KNOWS about the event stream and the "came from outside" marker, and knows
|
|
106
|
+
* no concrete event type: which bridges exist is the application's domain.
|
|
107
|
+
*
|
|
108
|
+
* ## Inbound only, even where the wire is two-way
|
|
109
|
+
*
|
|
110
|
+
* A WebSocket can send and a bridge cannot, deliberately. A bridge translates
|
|
111
|
+
* what arrived into a fact the rest of the application already understands, and
|
|
112
|
+
* a bridge that also sends becomes the one object that both starts and finishes
|
|
113
|
+
* a conversation — untestable without a socket, and the place every screen
|
|
114
|
+
* eventually reaches into. Sending belongs to whatever already owns the action:
|
|
115
|
+
* a gateway holding the channel, or a ViewModel calling it.
|
|
116
|
+
*
|
|
117
|
+
* ## Why subscriptions are collected rather than forgotten
|
|
118
|
+
*
|
|
119
|
+
* A bridge that subscribes in its constructor and never unsubscribes cannot
|
|
120
|
+
* notice the problem while it lives exactly as long as the application. In a
|
|
121
|
+
* package it is different — the plugin is removed, the instance is disposed, a
|
|
122
|
+
* dev server reloads the module — and a leftover subscription keeps triggering a
|
|
123
|
+
* dead instance's scenarios.
|
|
124
|
+
*/
|
|
125
|
+
declare abstract class ALankaStreamBridge {
|
|
126
|
+
private readonly unsubscribes;
|
|
127
|
+
protected readonly stream: ILankaServerEventTransport;
|
|
128
|
+
protected readonly trigger: ILankaStreamTriggerContext;
|
|
129
|
+
constructor(stream: ILankaServerEventTransport, trigger: ILankaStreamTriggerContext);
|
|
130
|
+
/** Subscribes the bridge to its events. Called by the plugin on install. */
|
|
131
|
+
abstract register(): void;
|
|
132
|
+
/** Removes everything the bridge subscribed to. */
|
|
133
|
+
dispose(): void;
|
|
134
|
+
/**
|
|
135
|
+
* Subscribes to an event type; the handler runs with the "from outside"
|
|
136
|
+
* marker.
|
|
137
|
+
*
|
|
138
|
+
* The marker is set HERE rather than left to each bridge: a handler that
|
|
139
|
+
* forgot it is indistinguishable from a user action, and the difference is
|
|
140
|
+
* silent — the screen simply behaves oddly in a rare case.
|
|
141
|
+
*/
|
|
142
|
+
protected on(eventType: string, handler: TLankaStreamEventCallback): void;
|
|
143
|
+
/** The same for an event with no payload. */
|
|
144
|
+
protected onSignal(eventType: string, handler: () => void): void;
|
|
145
|
+
/** Catch-up for what was missed while disconnected. */
|
|
146
|
+
protected onReconnect(handler: () => void): void;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** What the bridges factory is handed: the connection, and the marker. */
|
|
150
|
+
interface ILankaStreamPluginContext {
|
|
151
|
+
stream: ILankaServerEventTransport;
|
|
152
|
+
trigger: ILankaStreamTriggerContext;
|
|
153
|
+
}
|
|
154
|
+
interface ILankaStreamPluginConfig {
|
|
155
|
+
/** The connection this plugin owns for its lifetime. */
|
|
156
|
+
transport: ILankaServerEventTransport;
|
|
157
|
+
/**
|
|
158
|
+
* The application's bridges, created by it — the framework knows no event type.
|
|
159
|
+
*
|
|
160
|
+
* A factory rather than ready objects: a bridge needs the transport and the
|
|
161
|
+
* "from outside" marker, and both belong to THIS plugin installation. The same
|
|
162
|
+
* bridge declaration can therefore be handed to two instances in one process.
|
|
163
|
+
*/
|
|
164
|
+
bridges?: (context: ILankaStreamPluginContext) => readonly ALankaStreamBridge[];
|
|
165
|
+
/**
|
|
166
|
+
* Connect as soon as the plugin is registered. Off by default.
|
|
167
|
+
*
|
|
168
|
+
* Deliberately off: the stream is opened for an AUTHENTICATED user, and when
|
|
169
|
+
* that happens is the application's knowledge. A plugin that connected by
|
|
170
|
+
* itself would open a connection on the sign-in screen.
|
|
171
|
+
*/
|
|
172
|
+
connectOnInstall?: boolean;
|
|
173
|
+
/**
|
|
174
|
+
* The name the plugin is registered under. Defaults to `lanka/stream`.
|
|
175
|
+
*
|
|
176
|
+
* A protocol package passes its own npm name, so a duplicate registration
|
|
177
|
+
* names the package a reader can go and look at. Registering twice is refused
|
|
178
|
+
* by the registry, and two copies of one connection is exactly the mistake
|
|
179
|
+
* worth refusing.
|
|
180
|
+
*/
|
|
181
|
+
name?: string;
|
|
182
|
+
}
|
|
183
|
+
interface ILankaStreamPlugin extends ILankaPlugin {
|
|
184
|
+
/** The connection: subscribe, connect, disconnect. */
|
|
185
|
+
readonly stream: ILankaServerEventTransport;
|
|
186
|
+
/** The "this change came from the server" marker. */
|
|
187
|
+
readonly trigger: ILankaStreamTriggerContext;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* A pushing connection wired into the framework's lifetime.
|
|
191
|
+
*
|
|
192
|
+
* Usable directly — hand it any `ILankaServerEventTransport` and your bridges —
|
|
193
|
+
* and it is also what every protocol plugin in this repository is built on, so
|
|
194
|
+
* the four of them cannot disagree about installation order.
|
|
195
|
+
*
|
|
196
|
+
* ## The order in teardown is the whole point
|
|
197
|
+
*
|
|
198
|
+
* Bridges are detached BEFORE the connection is closed. The other order leaves
|
|
199
|
+
* them a window in which a dying connection delivers one last event into
|
|
200
|
+
* scenarios belonging to an instance that is being disposed.
|
|
201
|
+
*/
|
|
202
|
+
declare const lankaStream: (config: ILankaStreamPluginConfig) => ILankaStreamPlugin;
|
|
203
|
+
|
|
204
|
+
/** What every pushing connection is configured with, whatever carries it. */
|
|
205
|
+
interface ILankaStreamConfig {
|
|
206
|
+
/** How many reconnect attempts before giving up. Defaults to 10. */
|
|
207
|
+
maxReconnectAttempts?: number;
|
|
208
|
+
/** The first pause between attempts; it doubles from there. Defaults to 1s. */
|
|
209
|
+
reconnectDelayMs?: number;
|
|
210
|
+
/** Ceiling on the pause between attempts. Defaults to 30 seconds. */
|
|
211
|
+
maxReconnectDelayMs?: number;
|
|
212
|
+
/**
|
|
213
|
+
* Refreshes authorization when attempts are exhausted. `true` means try again.
|
|
214
|
+
*
|
|
215
|
+
* A function, not a URL: a framework that knew the endpoint would also know
|
|
216
|
+
* the response shape and how the session is stored.
|
|
217
|
+
*/
|
|
218
|
+
refreshAuth?: () => Promise<boolean>;
|
|
219
|
+
/** The session is lost for good: the application signs the user out. */
|
|
220
|
+
onSessionLost?: () => void;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* How a subclass reports what happened on the wire.
|
|
224
|
+
*
|
|
225
|
+
* Three facts and no more, because they are the three the ladder above branches
|
|
226
|
+
* on. A subclass able to reach further into the base could reset the attempt
|
|
227
|
+
* counter or fire a reconnect by hand, which is exactly the state this class
|
|
228
|
+
* exists to own.
|
|
229
|
+
*/
|
|
230
|
+
interface ILankaStreamTransportHandlers {
|
|
231
|
+
/**
|
|
232
|
+
* The connection is up and ready to carry subscriptions.
|
|
233
|
+
*
|
|
234
|
+
* Called by the subclass when the far end is actually usable — after a
|
|
235
|
+
* handshake, not merely after a socket opened — because the base answers it by
|
|
236
|
+
* subscribing to every event type somebody is waiting for.
|
|
237
|
+
*/
|
|
238
|
+
opened: () => void;
|
|
239
|
+
/** A named event arrived. */
|
|
240
|
+
received: (eventType: string, payload: Record<string, unknown>) => void;
|
|
241
|
+
/** The connection is gone: dropped, refused, or closed by the far end. */
|
|
242
|
+
lost: () => void;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The half of a pushing connection that is the same for every protocol.
|
|
246
|
+
*
|
|
247
|
+
* Four transports in this repository — server-sent events, a WebSocket, a
|
|
248
|
+
* `graphql-ws` subscription and a gRPC server stream — differ in how a
|
|
249
|
+
* connection is opened and in how bytes become a named event. They do not
|
|
250
|
+
* differ in anything below:
|
|
251
|
+
*
|
|
252
|
+
* - who is listening to which event type, and dispatch to a COPY of that set;
|
|
253
|
+
* - the reconnect ladder: growing backoff with a ceiling, an attempt count that
|
|
254
|
+
* only resets on a connection that actually opened, one auth refresh at the
|
|
255
|
+
* end of it, and an explicit disconnect that outranks everything scheduled;
|
|
256
|
+
* - `onReconnect` meaning "you missed something", so it never fires the first
|
|
257
|
+
* time.
|
|
258
|
+
*
|
|
259
|
+
* A subclass writes `open` and `close`, and `subscribeTo` / `unsubscribeFrom`
|
|
260
|
+
* when the wire has to be told which events are wanted.
|
|
261
|
+
*
|
|
262
|
+
* ## A subclass never says "connected"
|
|
263
|
+
*
|
|
264
|
+
* Everything the ladder decides is decided here, off the three handlers. That is
|
|
265
|
+
* what keeps a protocol package free of the bug this shape is prone to:
|
|
266
|
+
* resetting the attempt counter where reconnection STARTS rather than where it
|
|
267
|
+
* SUCCEEDS, after which a ten-attempt ceiling exists, reads as a guard, and can
|
|
268
|
+
* never fire.
|
|
269
|
+
*/
|
|
270
|
+
declare abstract class ALankaStreamTransport implements ILankaServerEventTransport {
|
|
271
|
+
private readonly listeners;
|
|
272
|
+
private readonly reconnectCallbacks;
|
|
273
|
+
private readonly config;
|
|
274
|
+
private readonly handlers;
|
|
275
|
+
private reconnectTimer;
|
|
276
|
+
private reconnectAttempts;
|
|
277
|
+
/** A connection exists as far as this class is concerned. */
|
|
278
|
+
private live;
|
|
279
|
+
/**
|
|
280
|
+
* A connection has opened at least once.
|
|
281
|
+
*
|
|
282
|
+
* Separate from `hadError`, and both are needed. A first attempt that FAILS
|
|
283
|
+
* and a second that succeeds is not a reconnection — nothing was ever
|
|
284
|
+
* delivered, so nothing was missed — and announcing one there makes every
|
|
285
|
+
* screen refetch the data it has just loaded.
|
|
286
|
+
*/
|
|
287
|
+
private everConnected;
|
|
288
|
+
/** The last connection ended badly, so the next `opened` is a RE-connection. */
|
|
289
|
+
private hadError;
|
|
290
|
+
/** Disconnected EXPLICITLY: everything scheduled after this must die. */
|
|
291
|
+
private stopped;
|
|
292
|
+
constructor(config?: ILankaStreamConfig);
|
|
293
|
+
/**
|
|
294
|
+
* Whether this engine can carry the connection at all.
|
|
295
|
+
*
|
|
296
|
+
* `true` unless a subclass says otherwise, because most transports need
|
|
297
|
+
* nothing the platform might be missing. Where one does, the answer is a
|
|
298
|
+
* `typeof` guard: an engine without the global gets the plugin switched off
|
|
299
|
+
* for the session rather than a `ReferenceError` from inside the framework.
|
|
300
|
+
*/
|
|
301
|
+
isSupported(): boolean;
|
|
302
|
+
connect(): void;
|
|
303
|
+
disconnect(): void;
|
|
304
|
+
/** Subscribes to an event type. Returns an unsubscribe function. */
|
|
305
|
+
on(eventType: string, callback: TLankaStreamEventCallback): () => void;
|
|
306
|
+
/** Notification after EVERY reconnection. Returns an unsubscribe function. */
|
|
307
|
+
onReconnect(callback: TLankaStreamReconnectCallback): () => void;
|
|
308
|
+
/**
|
|
309
|
+
* Opens the connection and reports through the handlers.
|
|
310
|
+
*
|
|
311
|
+
* Called once per attempt. Throwing is allowed and means "this engine cannot
|
|
312
|
+
* do it": the transport goes quiet instead of retrying.
|
|
313
|
+
*/
|
|
314
|
+
protected abstract open(handlers: ILankaStreamTransportHandlers): void;
|
|
315
|
+
/**
|
|
316
|
+
* Closes whatever `open` opened.
|
|
317
|
+
*
|
|
318
|
+
* Called at most ONCE per connection, and never for one `open` did not finish
|
|
319
|
+
* — so a subclass may assume it holds what it created. The base owns that
|
|
320
|
+
* guarantee (see `acceptLoss`) precisely so a subclass does not carry a flag
|
|
321
|
+
* to make it true.
|
|
322
|
+
*/
|
|
323
|
+
protected abstract close(): void;
|
|
324
|
+
/**
|
|
325
|
+
* Tells the wire that an event type is wanted, where the wire needs telling.
|
|
326
|
+
*
|
|
327
|
+
* Called for every already-known type as soon as a connection opens, and for
|
|
328
|
+
* each new one after that — so a subclass never has to remember what was
|
|
329
|
+
* subscribed before the link came back.
|
|
330
|
+
*/
|
|
331
|
+
protected subscribeTo(eventType: string): void;
|
|
332
|
+
/** The reverse, when the last subscriber of a type goes away. */
|
|
333
|
+
protected unsubscribeFrom(eventType: string): void;
|
|
334
|
+
/** Every event type somebody is waiting for right now. */
|
|
335
|
+
protected subscribedEventTypes(): readonly string[];
|
|
336
|
+
private acceptOpen;
|
|
337
|
+
private acceptLoss;
|
|
338
|
+
private forget;
|
|
339
|
+
private dispatch;
|
|
340
|
+
private clearReconnect;
|
|
341
|
+
private scheduleReconnect;
|
|
342
|
+
/**
|
|
343
|
+
* The attempts are spent: refresh the session once, or report it lost.
|
|
344
|
+
*
|
|
345
|
+
* `stopped` is re-read after the await. The refresh is a network call of
|
|
346
|
+
* unknown length, and an application that signed the user out while it was in
|
|
347
|
+
* flight must not be handed a connection afterwards.
|
|
348
|
+
*/
|
|
349
|
+
private lastResort;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* A bridge's protected surface, handed to whoever writes one by calling.
|
|
354
|
+
*
|
|
355
|
+
* The names match `ALankaStreamBridge`'s protected members exactly: a consumer
|
|
356
|
+
* who switches styles moves `this.on(...)` to `on(...)` and changes nothing
|
|
357
|
+
* else. Checked by `scripts/check-parity.mjs`.
|
|
358
|
+
*/
|
|
359
|
+
interface ILankaStreamBridgeContext {
|
|
360
|
+
/** Subscribes to an event type; the handler runs with the "from outside" marker. */
|
|
361
|
+
on: (eventType: string, handler: TLankaStreamEventCallback) => void;
|
|
362
|
+
/** The same for an event with no payload. */
|
|
363
|
+
onSignal: (eventType: string, handler: () => void) => void;
|
|
364
|
+
/** Catch-up for what was missed while disconnected. */
|
|
365
|
+
onReconnect: (handler: () => void) => void;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* A bridge, without writing a class.
|
|
370
|
+
*
|
|
371
|
+
* What a bridge does is one function — subscribe these events, trigger those
|
|
372
|
+
* scenarios — and the class exists to carry the subscriptions and the "from
|
|
373
|
+
* outside" marker, which this carries identically: it IS an `ALankaStreamBridge`.
|
|
374
|
+
*
|
|
375
|
+
* The plugin receives what a class-style bridge would give it, `dispose`
|
|
376
|
+
* included, so nothing above knows which style wrote it.
|
|
377
|
+
*
|
|
378
|
+
* Two steps rather than one call: a bridge needs the transport and the marker,
|
|
379
|
+
* and both belong to the plugin INSTALLATION rather than to the module that
|
|
380
|
+
* declares the bridge. So the declaration is written once at module level and
|
|
381
|
+
* the plugin applies it, which is what lets the same bridge be handed to two
|
|
382
|
+
* instances in one process.
|
|
383
|
+
*/
|
|
384
|
+
declare const createLankaStreamBridge: (register: (context: ILankaStreamBridgeContext) => void) => (stream: ILankaServerEventTransport, trigger: ILankaStreamTriggerContext) => ALankaStreamBridge;
|
|
385
|
+
|
|
386
|
+
export { ALankaStreamBridge, ALankaStreamTransport, type ILankaServerEventTransport, type ILankaStreamBridgeContext, type ILankaStreamConfig, type ILankaStreamPlugin, type ILankaStreamPluginConfig, type ILankaStreamPluginContext, type ILankaStreamTransportHandlers, type ILankaStreamTriggerContext, type TLankaStreamEventCallback, type TLankaStreamReconnectCallback, createLankaStreamBridge, createLankaStreamTriggerContext, lankaStream };
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// src/stream/_factories/create-lanka-stream-trigger-context/createLankaStreamTriggerContext.ts
|
|
2
|
+
var createLankaStreamTriggerContext = () => {
|
|
3
|
+
let active = false;
|
|
4
|
+
return {
|
|
5
|
+
isActive: () => active,
|
|
6
|
+
run(fn) {
|
|
7
|
+
const previous = active;
|
|
8
|
+
active = true;
|
|
9
|
+
try {
|
|
10
|
+
return fn();
|
|
11
|
+
} finally {
|
|
12
|
+
active = previous;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/stream/lanka-stream/lankaStream.ts
|
|
19
|
+
var lankaStream = (config) => {
|
|
20
|
+
const stream = config.transport;
|
|
21
|
+
const trigger = createLankaStreamTriggerContext();
|
|
22
|
+
return {
|
|
23
|
+
name: config.name ?? "lanka/stream",
|
|
24
|
+
stream,
|
|
25
|
+
trigger,
|
|
26
|
+
install() {
|
|
27
|
+
const bridges = config.bridges?.({ stream, trigger }) ?? [];
|
|
28
|
+
for (const bridge of bridges) bridge.register();
|
|
29
|
+
if (config.connectOnInstall) stream.connect();
|
|
30
|
+
return () => {
|
|
31
|
+
for (const bridge of bridges) bridge.dispose();
|
|
32
|
+
stream.disconnect();
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// src/stream/_abstractions/lanka-stream-transport/ALankaStreamTransport.ts
|
|
39
|
+
var ALankaStreamTransport = class {
|
|
40
|
+
listeners = /* @__PURE__ */ new Map();
|
|
41
|
+
reconnectCallbacks = /* @__PURE__ */ new Set();
|
|
42
|
+
config;
|
|
43
|
+
handlers;
|
|
44
|
+
reconnectTimer = null;
|
|
45
|
+
reconnectAttempts = 0;
|
|
46
|
+
/** A connection exists as far as this class is concerned. */
|
|
47
|
+
live = false;
|
|
48
|
+
/**
|
|
49
|
+
* A connection has opened at least once.
|
|
50
|
+
*
|
|
51
|
+
* Separate from `hadError`, and both are needed. A first attempt that FAILS
|
|
52
|
+
* and a second that succeeds is not a reconnection — nothing was ever
|
|
53
|
+
* delivered, so nothing was missed — and announcing one there makes every
|
|
54
|
+
* screen refetch the data it has just loaded.
|
|
55
|
+
*/
|
|
56
|
+
everConnected = false;
|
|
57
|
+
/** The last connection ended badly, so the next `opened` is a RE-connection. */
|
|
58
|
+
hadError = false;
|
|
59
|
+
/** Disconnected EXPLICITLY: everything scheduled after this must die. */
|
|
60
|
+
stopped = false;
|
|
61
|
+
constructor(config = {}) {
|
|
62
|
+
this.config = config;
|
|
63
|
+
this.handlers = {
|
|
64
|
+
opened: () => {
|
|
65
|
+
this.acceptOpen();
|
|
66
|
+
},
|
|
67
|
+
received: (eventType, payload) => {
|
|
68
|
+
this.dispatch(eventType, payload);
|
|
69
|
+
},
|
|
70
|
+
lost: () => {
|
|
71
|
+
this.acceptLoss();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether this engine can carry the connection at all.
|
|
77
|
+
*
|
|
78
|
+
* `true` unless a subclass says otherwise, because most transports need
|
|
79
|
+
* nothing the platform might be missing. Where one does, the answer is a
|
|
80
|
+
* `typeof` guard: an engine without the global gets the plugin switched off
|
|
81
|
+
* for the session rather than a `ReferenceError` from inside the framework.
|
|
82
|
+
*/
|
|
83
|
+
isSupported() {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
connect() {
|
|
87
|
+
if (this.live) return;
|
|
88
|
+
if (!this.isSupported()) return;
|
|
89
|
+
this.stopped = false;
|
|
90
|
+
this.live = true;
|
|
91
|
+
try {
|
|
92
|
+
this.open(this.handlers);
|
|
93
|
+
} catch {
|
|
94
|
+
this.live = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
disconnect() {
|
|
98
|
+
this.stopped = true;
|
|
99
|
+
this.clearReconnect();
|
|
100
|
+
if (!this.live) return;
|
|
101
|
+
this.live = false;
|
|
102
|
+
this.close();
|
|
103
|
+
}
|
|
104
|
+
/** Subscribes to an event type. Returns an unsubscribe function. */
|
|
105
|
+
on(eventType, callback) {
|
|
106
|
+
let callbacks = this.listeners.get(eventType);
|
|
107
|
+
if (!callbacks) {
|
|
108
|
+
callbacks = /* @__PURE__ */ new Set();
|
|
109
|
+
this.listeners.set(eventType, callbacks);
|
|
110
|
+
if (this.live) this.subscribeTo(eventType);
|
|
111
|
+
}
|
|
112
|
+
callbacks.add(callback);
|
|
113
|
+
return () => {
|
|
114
|
+
this.forget(eventType, callback);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Notification after EVERY reconnection. Returns an unsubscribe function. */
|
|
118
|
+
onReconnect(callback) {
|
|
119
|
+
this.reconnectCallbacks.add(callback);
|
|
120
|
+
return () => {
|
|
121
|
+
this.reconnectCallbacks.delete(callback);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Tells the wire that an event type is wanted, where the wire needs telling.
|
|
126
|
+
*
|
|
127
|
+
* Called for every already-known type as soon as a connection opens, and for
|
|
128
|
+
* each new one after that — so a subclass never has to remember what was
|
|
129
|
+
* subscribed before the link came back.
|
|
130
|
+
*/
|
|
131
|
+
subscribeTo(eventType) {
|
|
132
|
+
void eventType;
|
|
133
|
+
}
|
|
134
|
+
/** The reverse, when the last subscriber of a type goes away. */
|
|
135
|
+
unsubscribeFrom(eventType) {
|
|
136
|
+
void eventType;
|
|
137
|
+
}
|
|
138
|
+
/** Every event type somebody is waiting for right now. */
|
|
139
|
+
subscribedEventTypes() {
|
|
140
|
+
return [...this.listeners.keys()];
|
|
141
|
+
}
|
|
142
|
+
acceptOpen() {
|
|
143
|
+
for (const eventType of this.listeners.keys()) this.subscribeTo(eventType);
|
|
144
|
+
if (this.everConnected && this.hadError) {
|
|
145
|
+
for (const callback of [...this.reconnectCallbacks]) callback();
|
|
146
|
+
}
|
|
147
|
+
this.everConnected = true;
|
|
148
|
+
this.hadError = false;
|
|
149
|
+
this.reconnectAttempts = 0;
|
|
150
|
+
}
|
|
151
|
+
acceptLoss() {
|
|
152
|
+
if (!this.live) return;
|
|
153
|
+
this.hadError = true;
|
|
154
|
+
this.live = false;
|
|
155
|
+
this.close();
|
|
156
|
+
void this.scheduleReconnect();
|
|
157
|
+
}
|
|
158
|
+
forget(eventType, callback) {
|
|
159
|
+
const current = this.listeners.get(eventType);
|
|
160
|
+
if (!current) return;
|
|
161
|
+
current.delete(callback);
|
|
162
|
+
if (current.size > 0) return;
|
|
163
|
+
this.listeners.delete(eventType);
|
|
164
|
+
if (this.live) this.unsubscribeFrom(eventType);
|
|
165
|
+
}
|
|
166
|
+
dispatch(eventType, payload) {
|
|
167
|
+
const callbacks = this.listeners.get(eventType);
|
|
168
|
+
if (!callbacks) return;
|
|
169
|
+
for (const callback of [...callbacks]) callback(payload);
|
|
170
|
+
}
|
|
171
|
+
clearReconnect() {
|
|
172
|
+
if (!this.reconnectTimer) return;
|
|
173
|
+
clearTimeout(this.reconnectTimer);
|
|
174
|
+
this.reconnectTimer = null;
|
|
175
|
+
}
|
|
176
|
+
async scheduleReconnect() {
|
|
177
|
+
if (this.stopped) return;
|
|
178
|
+
if (this.reconnectAttempts >= (this.config.maxReconnectAttempts ?? 10)) {
|
|
179
|
+
await this.lastResort();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const first = this.config.reconnectDelayMs ?? 1e3;
|
|
183
|
+
const delay = Math.min(
|
|
184
|
+
first * 2 ** this.reconnectAttempts,
|
|
185
|
+
this.config.maxReconnectDelayMs ?? 3e4
|
|
186
|
+
);
|
|
187
|
+
this.reconnectAttempts += 1;
|
|
188
|
+
this.reconnectTimer = setTimeout(() => {
|
|
189
|
+
this.reconnectTimer = null;
|
|
190
|
+
if (!this.stopped) this.connect();
|
|
191
|
+
}, delay);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The attempts are spent: refresh the session once, or report it lost.
|
|
195
|
+
*
|
|
196
|
+
* `stopped` is re-read after the await. The refresh is a network call of
|
|
197
|
+
* unknown length, and an application that signed the user out while it was in
|
|
198
|
+
* flight must not be handed a connection afterwards.
|
|
199
|
+
*/
|
|
200
|
+
async lastResort() {
|
|
201
|
+
const refreshed = await this.config.refreshAuth?.() ?? false;
|
|
202
|
+
if (this.stopped) return;
|
|
203
|
+
if (!refreshed) {
|
|
204
|
+
this.config.onSessionLost?.();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
this.reconnectAttempts = 0;
|
|
208
|
+
this.connect();
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// src/stream/_abstractions/lanka-stream-bridge/ALankaStreamBridge.ts
|
|
213
|
+
var ALankaStreamBridge = class {
|
|
214
|
+
unsubscribes = [];
|
|
215
|
+
stream;
|
|
216
|
+
trigger;
|
|
217
|
+
// Fields declared explicitly: `erasableSyntaxOnly` forbids parameter
|
|
218
|
+
// properties, the one TypeScript construct that emits code.
|
|
219
|
+
//
|
|
220
|
+
// The constructor is PUBLIC: the class is abstract and cannot be constructed
|
|
221
|
+
// on its own, while `protected` is inherited — and the application's subclass
|
|
222
|
+
// would then be unreachable to the code that creates it.
|
|
223
|
+
constructor(stream, trigger) {
|
|
224
|
+
this.stream = stream;
|
|
225
|
+
this.trigger = trigger;
|
|
226
|
+
}
|
|
227
|
+
/** Removes everything the bridge subscribed to. */
|
|
228
|
+
dispose() {
|
|
229
|
+
for (const unsubscribe of this.unsubscribes) unsubscribe();
|
|
230
|
+
this.unsubscribes.length = 0;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Subscribes to an event type; the handler runs with the "from outside"
|
|
234
|
+
* marker.
|
|
235
|
+
*
|
|
236
|
+
* The marker is set HERE rather than left to each bridge: a handler that
|
|
237
|
+
* forgot it is indistinguishable from a user action, and the difference is
|
|
238
|
+
* silent — the screen simply behaves oddly in a rare case.
|
|
239
|
+
*/
|
|
240
|
+
on(eventType, handler) {
|
|
241
|
+
this.unsubscribes.push(
|
|
242
|
+
this.stream.on(eventType, (payload) => {
|
|
243
|
+
this.trigger.run(() => handler(payload));
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
/** The same for an event with no payload. */
|
|
248
|
+
onSignal(eventType, handler) {
|
|
249
|
+
this.on(eventType, () => handler());
|
|
250
|
+
}
|
|
251
|
+
/** Catch-up for what was missed while disconnected. */
|
|
252
|
+
onReconnect(handler) {
|
|
253
|
+
this.unsubscribes.push(
|
|
254
|
+
this.stream.onReconnect(() => {
|
|
255
|
+
this.trigger.run(handler);
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// src/stream/_factories/create-lanka-stream-bridge/createLankaStreamBridge.ts
|
|
262
|
+
var createLankaStreamBridge = (register) => (stream, trigger) => {
|
|
263
|
+
class FunctionalBridge extends ALankaStreamBridge {
|
|
264
|
+
register() {
|
|
265
|
+
register({
|
|
266
|
+
on: (eventType, handler) => {
|
|
267
|
+
this.on(eventType, handler);
|
|
268
|
+
},
|
|
269
|
+
onSignal: (eventType, handler) => {
|
|
270
|
+
this.onSignal(eventType, handler);
|
|
271
|
+
},
|
|
272
|
+
onReconnect: (handler) => {
|
|
273
|
+
this.onReconnect(handler);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return new FunctionalBridge(stream, trigger);
|
|
279
|
+
};
|
|
280
|
+
export {
|
|
281
|
+
ALankaStreamBridge,
|
|
282
|
+
ALankaStreamTransport,
|
|
283
|
+
createLankaStreamBridge,
|
|
284
|
+
createLankaStreamTriggerContext,
|
|
285
|
+
lankaStream
|
|
286
|
+
};
|
|
287
|
+
//# sourceMappingURL=index.js.map
|