@wcstack/share 1.19.1 → 1.21.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.ja.md +6 -0
- package/README.md +6 -0
- package/dist/index.d.ts +107 -14
- package/dist/index.esm.js +494 -58
- package/dist/index.esm.js.map +1 -1
- package/dist/index.esm.min.js +1 -1
- package/dist/index.esm.min.js.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -36,19 +36,398 @@ function setConfig(partialConfig) {
|
|
|
36
36
|
frozenConfig = null;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// ===========================================================================
|
|
40
|
+
// AUTO-GENERATED FILE - DO NOT EDIT.
|
|
41
|
+
// Generated from /io-core/operation-lane.ts by scripts/sync-io-core.mjs.
|
|
42
|
+
// Run `node scripts/sync-io-core.mjs` after editing the source.
|
|
43
|
+
// ===========================================================================
|
|
44
|
+
/**
|
|
45
|
+
* 1 レーン = 独立した排他単位。Core が 1 つ以上所有する (module singleton にしない —
|
|
46
|
+
* 複数 <wcs-fetch> 間で漏れるため)。
|
|
47
|
+
*/
|
|
48
|
+
class OperationLane {
|
|
49
|
+
laneKey;
|
|
50
|
+
policy;
|
|
51
|
+
_ownerGeneration = 0;
|
|
52
|
+
_latestEpoch = 0;
|
|
53
|
+
_nextOperationId = 1;
|
|
54
|
+
// latest / queue / exhaust の単一 active。queue は head を指す。
|
|
55
|
+
_activeOperationId = undefined;
|
|
56
|
+
// overlap の active set (§5: 内部 bookkeeping のみ・observable 公開はしない)。
|
|
57
|
+
_activeOperationIds = new Set();
|
|
58
|
+
// queue policy の FIFO。
|
|
59
|
+
_queue = [];
|
|
60
|
+
_inFlightCount = 0;
|
|
61
|
+
// opId → 終端状態 (absence = pending)。
|
|
62
|
+
_terminal = new Map();
|
|
63
|
+
// claimTerminal で確定した outcome (finalize が 'committing' を最終値へ移す)。
|
|
64
|
+
_claimedOutcome = new Map();
|
|
65
|
+
// opId → AbortController (identity は opId が保証。cross-op clobber は構造上起きない)。
|
|
66
|
+
_controllers = new Map();
|
|
67
|
+
// opId → attempt 数。
|
|
68
|
+
_attempts = new Map();
|
|
69
|
+
_withSignal;
|
|
70
|
+
_trace;
|
|
71
|
+
constructor(laneKey, policy, options = {}) {
|
|
72
|
+
this.laneKey = laneKey;
|
|
73
|
+
this.policy = policy;
|
|
74
|
+
this._withSignal = options.withSignal ?? false;
|
|
75
|
+
this._trace = options.trace;
|
|
76
|
+
}
|
|
77
|
+
get ownerGeneration() {
|
|
78
|
+
return this._ownerGeneration;
|
|
79
|
+
}
|
|
80
|
+
get inFlightCount() {
|
|
81
|
+
return this._inFlightCount;
|
|
82
|
+
}
|
|
83
|
+
get latestEpoch() {
|
|
84
|
+
return this._latestEpoch;
|
|
85
|
+
}
|
|
86
|
+
get activeOperationId() {
|
|
87
|
+
return this._activeOperationId;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 新しい要求の到着。arrival policy を適用し ticket + 最初の attempt を発行する。
|
|
91
|
+
* exhaust で実行中の場合だけ null を返す (新要求を ticket 化せず拒否 = 冪等 no-op)。
|
|
92
|
+
*/
|
|
93
|
+
begin() {
|
|
94
|
+
let supersedeEpoch;
|
|
95
|
+
switch (this.policy) {
|
|
96
|
+
case "latest": {
|
|
97
|
+
// latestEpoch を進め、旧 active を abort (可能なら)。旧 ticket は settle 時に
|
|
98
|
+
// eligibility 不一致で stale となる。
|
|
99
|
+
supersedeEpoch = ++this._latestEpoch;
|
|
100
|
+
if (this._activeOperationId !== undefined) {
|
|
101
|
+
this._abortController(this._activeOperationId);
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case "exhaust": {
|
|
106
|
+
// 実行中なら新要求を拒否 (呼び出し側は既存結果へ合流)。
|
|
107
|
+
if (this._activeOperationId !== undefined) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const operationId = this._nextOperationId++;
|
|
114
|
+
const ticket = {
|
|
115
|
+
operationId,
|
|
116
|
+
ownerGeneration: this._ownerGeneration,
|
|
117
|
+
laneKey: this.laneKey,
|
|
118
|
+
policy: this.policy,
|
|
119
|
+
supersedeEpoch,
|
|
120
|
+
};
|
|
121
|
+
switch (this.policy) {
|
|
122
|
+
case "latest":
|
|
123
|
+
case "exhaust":
|
|
124
|
+
this._activeOperationId = operationId;
|
|
125
|
+
break;
|
|
126
|
+
case "queue":
|
|
127
|
+
this._queue.push(ticket);
|
|
128
|
+
// 先頭だけを active にする (先行が完了するまで待つ)。
|
|
129
|
+
if (this._activeOperationId === undefined) {
|
|
130
|
+
this._activeOperationId = operationId;
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
case "overlap":
|
|
134
|
+
this._activeOperationIds.add(operationId);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
this._inFlightCount += 1;
|
|
138
|
+
this._attempts.set(operationId, 1);
|
|
139
|
+
const attempt = this._makeAttempt(operationId, 1);
|
|
140
|
+
if (this._trace !== undefined) {
|
|
141
|
+
this._trace({ type: "io:operation-started", operationId, laneKey: this.laneKey, policy: this.policy });
|
|
142
|
+
}
|
|
143
|
+
return { ticket, attempt };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* retry: 同じ operationId に新しい attempt を作る。attempt number と resource signal
|
|
147
|
+
* だけを更新する (§5)。既に終端した operation には作れない (null)。
|
|
148
|
+
*/
|
|
149
|
+
retry(ticket) {
|
|
150
|
+
if (ticket.ownerGeneration !== this._ownerGeneration)
|
|
151
|
+
return null;
|
|
152
|
+
if (this._terminal.has(ticket.operationId))
|
|
153
|
+
return null;
|
|
154
|
+
const previous = this._attempts.get(ticket.operationId);
|
|
155
|
+
if (previous === undefined)
|
|
156
|
+
return null;
|
|
157
|
+
const attemptNo = previous + 1;
|
|
158
|
+
this._attempts.set(ticket.operationId, attemptNo);
|
|
159
|
+
// 前の attempt の signal は破棄し、新しい controller を張る。
|
|
160
|
+
this._releaseController(ticket.operationId);
|
|
161
|
+
const attempt = this._makeAttempt(ticket.operationId, attemptNo);
|
|
162
|
+
if (this._trace !== undefined) {
|
|
163
|
+
this._trace({ type: "io:operation-retried", operationId: ticket.operationId, laneKey: this.laneKey, attempt: attemptNo });
|
|
164
|
+
}
|
|
165
|
+
return attempt;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* CommitGuard (§5.1)。外部可視の setter / event dispatch の直前に呼ぶ。
|
|
169
|
+
* (1) owner lifecycle generation 一致 (2) terminal settle 前 (3) policy eligibility。
|
|
170
|
+
*/
|
|
171
|
+
canCommit(ticket) {
|
|
172
|
+
if (ticket.ownerGeneration !== this._ownerGeneration)
|
|
173
|
+
return false;
|
|
174
|
+
const status = this._terminal.get(ticket.operationId);
|
|
175
|
+
// absence = pending / 'committing' = multi-setter commit 中。どちらも settle 前。
|
|
176
|
+
if (status !== undefined && status !== "committing")
|
|
177
|
+
return false;
|
|
178
|
+
return this._isEligible(ticket);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* terminal CAS (§5.1): pending → committing を claim する。勝者だけが true。
|
|
182
|
+
* eligibility / owner gen を満たさない場合も false。claim 後は commit 中となり、
|
|
183
|
+
* canCommit は各 setter の直前で再検査する (setter が同期 supersede しても取りこぼさない)。
|
|
184
|
+
*/
|
|
185
|
+
claimTerminal(ticket, outcome) {
|
|
186
|
+
if (ticket.ownerGeneration !== this._ownerGeneration)
|
|
187
|
+
return false;
|
|
188
|
+
if (this._terminal.has(ticket.operationId))
|
|
189
|
+
return false; // 既に committing / 終端
|
|
190
|
+
if (!this._isEligible(ticket))
|
|
191
|
+
return false;
|
|
192
|
+
this._terminal.set(ticket.operationId, "committing");
|
|
193
|
+
this._claimedOutcome.set(ticket.operationId, outcome);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
/** claim 済み outcome (timer が claim → catch が読む等)。未 claim なら undefined。 */
|
|
197
|
+
claimedOutcome(ticket) {
|
|
198
|
+
return this._claimedOutcome.get(ticket.operationId);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* operation の後始末。claim 済みなら outcome を確定し、未 claim なら stale-drop。
|
|
202
|
+
* controller を解放し in-flight を減らし、policy の bookkeeping を進める。冪等。
|
|
203
|
+
*/
|
|
204
|
+
finalize(ticket) {
|
|
205
|
+
const operationId = ticket.operationId;
|
|
206
|
+
const status = this._terminal.get(operationId);
|
|
207
|
+
if (status !== undefined && status !== "committing") {
|
|
208
|
+
// 既に確定済み。冪等に return。
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
let outcome;
|
|
212
|
+
if (status === "committing") {
|
|
213
|
+
outcome = this._claimedOutcome.get(operationId) ?? "stale";
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
// 一度も claim されなかった (supersede / dispose で eligibility を失った)。
|
|
217
|
+
outcome = "stale";
|
|
218
|
+
}
|
|
219
|
+
this._terminal.set(operationId, outcome);
|
|
220
|
+
this._claimedOutcome.delete(operationId);
|
|
221
|
+
this._releaseController(operationId);
|
|
222
|
+
this._attempts.delete(operationId);
|
|
223
|
+
if (this._inFlightCount > 0)
|
|
224
|
+
this._inFlightCount -= 1;
|
|
225
|
+
this._advanceBookkeeping(operationId);
|
|
226
|
+
if (this._trace !== undefined) {
|
|
227
|
+
if (outcome === "stale") {
|
|
228
|
+
this._trace({ type: "io:stale-dropped", operationId, laneKey: this.laneKey });
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
this._trace({ type: "io:operation-settled", operationId, laneKey: this.laneKey, outcome });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** operation の signal (resource 解放用)。withSignal でなければ undefined。 */
|
|
236
|
+
signalOf(ticket) {
|
|
237
|
+
return this._controllers.get(ticket.operationId)?.signal;
|
|
238
|
+
}
|
|
239
|
+
/** best-effort な resource 中断。正しさは owner gen / eligibility / terminal CAS が担う。 */
|
|
240
|
+
abort(ticket) {
|
|
241
|
+
this._abortController(ticket.operationId);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* 現在 active な operation を中断する (利用者による明示キャンセル)。epoch は進めない —
|
|
245
|
+
* 中断された operation は eligibility を保ったまま 'aborted' を claim できる
|
|
246
|
+
* (loading をクリアしつつ in-flight 状態を残す)。
|
|
247
|
+
*/
|
|
248
|
+
abortActive() {
|
|
249
|
+
if (this._activeOperationId !== undefined) {
|
|
250
|
+
this._abortController(this._activeOperationId);
|
|
251
|
+
}
|
|
252
|
+
for (const operationId of this._activeOperationIds) {
|
|
253
|
+
this._abortController(operationId);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* dispose (§4.1 world generation)。owner generation を bump して全 ticket を無効化し、
|
|
258
|
+
* 生きている controller を全て abort する。dispose 後に settle した operation は
|
|
259
|
+
* owner gen 不一致で外部 commit しない。retention gate (§10.3) のため live な
|
|
260
|
+
* 全 operation を即時に stale として finalize し、controller / attempt を解放する。
|
|
261
|
+
*/
|
|
262
|
+
disposeOwner() {
|
|
263
|
+
this._ownerGeneration += 1;
|
|
264
|
+
for (const operationId of Array.from(this._controllers.keys())) {
|
|
265
|
+
this._abortController(operationId);
|
|
266
|
+
// finalize は dispose 後 (terminal='stale') に early-return するため controller を
|
|
267
|
+
// 解放しない。retention gate (§10.3) を満たすためここで明示的に解放する。
|
|
268
|
+
this._releaseController(operationId);
|
|
269
|
+
}
|
|
270
|
+
for (const operationId of Array.from(this._attempts.keys())) {
|
|
271
|
+
if (!this._terminal.has(operationId)) {
|
|
272
|
+
this._terminal.set(operationId, "stale");
|
|
273
|
+
}
|
|
274
|
+
this._claimedOutcome.delete(operationId);
|
|
275
|
+
this._attempts.delete(operationId);
|
|
276
|
+
if (this._trace !== undefined) {
|
|
277
|
+
this._trace({ type: "io:stale-dropped", operationId, laneKey: this.laneKey });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
this._activeOperationId = undefined;
|
|
281
|
+
this._activeOperationIds.clear();
|
|
282
|
+
this._queue.length = 0;
|
|
283
|
+
this._inFlightCount = 0;
|
|
284
|
+
}
|
|
285
|
+
// --- internal ---
|
|
286
|
+
_makeAttempt(operationId, attemptNo) {
|
|
287
|
+
let signal;
|
|
288
|
+
// AbortController 不在環境(古い runtime / 一部 SSR)では degraded: signal なしで進む。
|
|
289
|
+
// 正しさは owner generation / eligibility / terminal CAS が担うため、native 中断が
|
|
290
|
+
// 無くても supersede / dispose は機能する(best-effort resource 中断が省かれるだけ)。
|
|
291
|
+
if (this._withSignal && typeof AbortController === "function") {
|
|
292
|
+
const controller = new AbortController();
|
|
293
|
+
this._controllers.set(operationId, controller);
|
|
294
|
+
signal = controller.signal;
|
|
295
|
+
}
|
|
296
|
+
return { operationId, attempt: attemptNo, signal };
|
|
297
|
+
}
|
|
298
|
+
_isEligible(ticket) {
|
|
299
|
+
switch (this.policy) {
|
|
300
|
+
case "latest":
|
|
301
|
+
return ticket.supersedeEpoch === this._latestEpoch;
|
|
302
|
+
case "queue":
|
|
303
|
+
case "exhaust":
|
|
304
|
+
return this._activeOperationId === ticket.operationId;
|
|
305
|
+
case "overlap":
|
|
306
|
+
return this._activeOperationIds.has(ticket.operationId);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
_advanceBookkeeping(operationId) {
|
|
310
|
+
switch (this.policy) {
|
|
311
|
+
case "latest":
|
|
312
|
+
case "exhaust":
|
|
313
|
+
if (this._activeOperationId === operationId) {
|
|
314
|
+
this._activeOperationId = undefined;
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
case "queue": {
|
|
318
|
+
// 完了した ticket を FIFO から取り除き、次の先頭を active にする。filter で
|
|
319
|
+
// 「先頭 / 非先頭 / 不在」を一様に扱う (finalize は冪等ガードを通った op のみ到達)。
|
|
320
|
+
const remaining = this._queue.filter((t) => t.operationId !== operationId);
|
|
321
|
+
this._queue.length = 0;
|
|
322
|
+
this._queue.push(...remaining);
|
|
323
|
+
this._activeOperationId = this._queue.length > 0 ? this._queue[0].operationId : undefined;
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case "overlap":
|
|
327
|
+
this._activeOperationIds.delete(operationId);
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
_abortController(operationId) {
|
|
332
|
+
const controller = this._controllers.get(operationId);
|
|
333
|
+
if (controller !== undefined && !controller.signal.aborted) {
|
|
334
|
+
controller.abort();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
_releaseController(operationId) {
|
|
338
|
+
this._controllers.delete(operationId);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ===========================================================================
|
|
343
|
+
// AUTO-GENERATED FILE - DO NOT EDIT.
|
|
344
|
+
// Generated from /io-core/platform-capability.ts by scripts/sync-io-core.mjs.
|
|
345
|
+
// Run `node scripts/sync-io-core.mjs` after editing the source.
|
|
346
|
+
// ===========================================================================
|
|
347
|
+
function isSecureContext() {
|
|
348
|
+
return globalThis.isSecureContext === true;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* capability を利用直前に評価して PlatformAssessment を作る。
|
|
352
|
+
* required が 1 つでも欠ければ readiness は "idle"(開始不可)、
|
|
353
|
+
* required 揃い + optional 欠けは "degraded"、全揃いは "ready"。
|
|
354
|
+
*/
|
|
355
|
+
function assessCapabilities(registry, options) {
|
|
356
|
+
const availability = new Map();
|
|
357
|
+
const evaluate = (id) => {
|
|
358
|
+
const spec = registry.get(id);
|
|
359
|
+
if (spec === undefined)
|
|
360
|
+
return "unknown";
|
|
361
|
+
return spec.probe() ? "available" : "missing";
|
|
362
|
+
};
|
|
363
|
+
let requiredAllAvailable = true;
|
|
364
|
+
for (const id of options.required) {
|
|
365
|
+
const a = evaluate(id);
|
|
366
|
+
availability.set(id, a);
|
|
367
|
+
if (a !== "available")
|
|
368
|
+
requiredAllAvailable = false;
|
|
369
|
+
}
|
|
370
|
+
let optionalAllAvailable = true;
|
|
371
|
+
for (const id of options.optional ?? []) {
|
|
372
|
+
const a = evaluate(id);
|
|
373
|
+
availability.set(id, a);
|
|
374
|
+
if (a !== "available")
|
|
375
|
+
optionalAllAvailable = false;
|
|
376
|
+
}
|
|
377
|
+
const readiness = !requiredAllAvailable ? "idle" : (optionalAllAvailable ? "ready" : "degraded");
|
|
378
|
+
// preconditions: 対象 capability のいずれかが要求する場合だけ評価する。
|
|
379
|
+
const allIds = [...options.required, ...(options.optional ?? [])];
|
|
380
|
+
const needsSecure = allIds.some((id) => registry.get(id)?.requiresSecureContext === true);
|
|
381
|
+
const needsActivation = allIds.some((id) => registry.get(id)?.requiresUserActivation === true);
|
|
382
|
+
const secureContext = needsSecure ? (isSecureContext() ? "satisfied" : "required") : "not-applicable";
|
|
383
|
+
const userActivation = needsActivation ? "required" : "not-applicable";
|
|
384
|
+
return {
|
|
385
|
+
availability,
|
|
386
|
+
permission: options.permission ?? "not-applicable",
|
|
387
|
+
readiness,
|
|
388
|
+
activity: options.activity ?? "inactive",
|
|
389
|
+
preconditions: { secureContext, userActivation },
|
|
390
|
+
epoch: options.epoch ?? 0,
|
|
391
|
+
lastError: options.lastError,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/** availability から「required がすべて available か」を判定するヘルパ(supported の最低条件)。 */
|
|
395
|
+
function requiredCapabilitiesAvailable(assessment, required) {
|
|
396
|
+
return required.every((id) => assessment.availability.get(id) === "available");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* shareCapabilities.ts
|
|
401
|
+
*
|
|
402
|
+
* Web Share node 固有の capability registry と error code。汎用の assess 機構・型は
|
|
403
|
+
* `./platformCapability.js`(/io-core/ から copy-distribution される生成ファイル)から
|
|
404
|
+
* import する。node 固有の宣言はこのハンドライトファイルに置き、生成コピーとは分離する。
|
|
405
|
+
*/
|
|
406
|
+
/** 安定した share error code(taxonomy)。値は公開キーとして固定。 */
|
|
407
|
+
const WCS_SHARE_ERROR_CODE = {
|
|
408
|
+
CapabilityMissing: "capability-missing",
|
|
409
|
+
ShareFailed: "share-failed",
|
|
410
|
+
};
|
|
411
|
+
/** share node の capability registry。文字列 ID を eval せず明示 probe を持つ。 */
|
|
412
|
+
const SHARE_CAPABILITIES = new Map([
|
|
413
|
+
["web.share", { probe: () => typeof globalThis.navigator?.share === "function", compatKey: "api.Navigator.share" }],
|
|
414
|
+
]);
|
|
415
|
+
|
|
39
416
|
/**
|
|
40
417
|
* Headless Web Share primitive. A thin, framework-agnostic wrapper around
|
|
41
418
|
* `navigator.share(data)` exposed through the wc-bindable protocol.
|
|
42
419
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
420
|
+
* Concurrency is owned by the shared `OperationLane` (io-core) with the `exhaust`
|
|
421
|
+
* policy: a share dialog is a single system-modal surface, so while one share() is
|
|
422
|
+
* in flight a new call is rejected as an idempotent no-op instead of starting a
|
|
423
|
+
* second `navigator.share()`. This replaces the earlier dispose-only `_gen` guard,
|
|
424
|
+
* which relied on the platform rejecting the second call with `InvalidStateError`
|
|
425
|
+
* — but that let the rejected second call reset/overwrite the still-pending first
|
|
426
|
+
* call's `error`/`loading` state. The lane's owner generation still invalidates any
|
|
427
|
+
* in-flight share() on dispose() (a late resolve fails the commit guard).
|
|
428
|
+
*
|
|
429
|
+
* `navigator.share()` accepts no `AbortSignal` and there is no platform mechanism
|
|
430
|
+
* to cancel an in-flight share dialog, so the lane runs with `withSignal: false`.
|
|
52
431
|
*/
|
|
53
432
|
class ShareCore extends EventTarget {
|
|
54
433
|
static wcBindable = {
|
|
@@ -59,29 +438,29 @@ class ShareCore extends EventTarget {
|
|
|
59
438
|
{ name: "loading", event: "wcs-share:loading-changed" },
|
|
60
439
|
{ name: "error", event: "wcs-share:error" },
|
|
61
440
|
{ name: "cancelled", event: "wcs-share:cancelled-changed" },
|
|
441
|
+
// Serializable failure taxonomy (stable code / phase / recoverable), or null.
|
|
442
|
+
// Additive bindable output; the existing `error` property/event are unchanged.
|
|
443
|
+
// Fires its own `wcs-share:error-info-changed` event; no getter, so the bound
|
|
444
|
+
// value is the event detail (mirrors `error` / `loading` / `cancelled`).
|
|
445
|
+
{ name: "errorInfo", event: "wcs-share:error-info-changed" },
|
|
62
446
|
],
|
|
63
447
|
commands: [
|
|
64
448
|
{ name: "share", async: true },
|
|
65
449
|
],
|
|
66
450
|
};
|
|
451
|
+
// Required capability (probed at call time, never at module eval). `web.share`
|
|
452
|
+
// is the only required API; there is no optional/degraded surface for share.
|
|
453
|
+
static REQUIRED_CAPABILITIES = ["web.share"];
|
|
67
454
|
_target;
|
|
68
455
|
_value = null;
|
|
69
456
|
_loading = false;
|
|
70
457
|
_error = null;
|
|
71
458
|
_cancelled = false;
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
// those cores need, because the platform allows only one open share dialog
|
|
78
|
-
// at a time (a second concurrent share() rejects with InvalidStateError on
|
|
79
|
-
// its own). Bumping `_gen` per call would instead let a fast-failing second
|
|
80
|
-
// call incorrectly invalidate a still-pending first call's eventual
|
|
81
|
-
// success. Also not bumped on the unsupported early-return — no
|
|
82
|
-
// asynchronous work is started, so there is no generation to protect
|
|
83
|
-
// (docs/web-share-tag-design.md §8).
|
|
84
|
-
_gen = 0;
|
|
459
|
+
_errorInfo = null;
|
|
460
|
+
// Concurrency lane (io-core). `exhaust`: only one share dialog at a time — a new
|
|
461
|
+
// begin() while active returns null (idempotent no-op). `withSignal: false`:
|
|
462
|
+
// navigator.share() has no AbortSignal. dispose() bumps the owner generation.
|
|
463
|
+
_lane = new OperationLane("share", "exhaust", { withSignal: false });
|
|
85
464
|
// SSR (§3.8): no asynchronous probe to await, so readiness is immediate.
|
|
86
465
|
_ready = Promise.resolve();
|
|
87
466
|
constructor(target) {
|
|
@@ -103,15 +482,42 @@ class ShareCore extends EventTarget {
|
|
|
103
482
|
get cancelled() {
|
|
104
483
|
return this._cancelled;
|
|
105
484
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
485
|
+
/**
|
|
486
|
+
* The last failure's serializable `WcsIoErrorInfo` (stable `code` / `phase` /
|
|
487
|
+
* `recoverable` / `capabilityId`), or null. Exposed as an additive wc-bindable
|
|
488
|
+
* property (event `wcs-share:error-info-changed`); the existing `error`
|
|
489
|
+
* property/event are unchanged.
|
|
490
|
+
*/
|
|
491
|
+
get errorInfo() {
|
|
492
|
+
return this._errorInfo;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Whether the required platform capability (`web.share`) is available right now —
|
|
496
|
+
* decided by call-time feature detection, not User-Agent. Core-only, additive.
|
|
497
|
+
*/
|
|
498
|
+
get supported() {
|
|
499
|
+
return requiredCapabilitiesAvailable(this.platformAssessment, ShareCore.REQUIRED_CAPABILITIES);
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Full platform assessment (availability / readiness / preconditions), probed at
|
|
503
|
+
* call time. Core-only opt-in dev / sidecar view.
|
|
504
|
+
*/
|
|
505
|
+
get platformAssessment() {
|
|
506
|
+
return assessCapabilities(SHARE_CAPABILITIES, {
|
|
507
|
+
required: ShareCore.REQUIRED_CAPABILITIES,
|
|
508
|
+
activity: this._loading ? "active" : "inactive",
|
|
509
|
+
lastError: this._errorInfo ?? undefined,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
// Lifecycle (§3.5). Share is command-driven with no subscription to establish,
|
|
513
|
+
// so observe() is an idempotent no-op that resolves once ready; dispose() bumps
|
|
514
|
+
// the lane's owner generation, invalidating any in-flight share() (a late resolve
|
|
515
|
+
// then fails the commit guard). There is nothing to abort or unsubscribe.
|
|
110
516
|
observe() {
|
|
111
517
|
return this._ready;
|
|
112
518
|
}
|
|
113
519
|
dispose() {
|
|
114
|
-
this.
|
|
520
|
+
this._lane.disposeOwner();
|
|
115
521
|
}
|
|
116
522
|
_setLoading(loading) {
|
|
117
523
|
if (this._loading === loading)
|
|
@@ -156,61 +562,88 @@ class ShareCore extends EventTarget {
|
|
|
156
562
|
bubbles: true,
|
|
157
563
|
}));
|
|
158
564
|
}
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
565
|
+
// Single mutation point for `errorInfo`, mirroring `_setError`'s same-value guard
|
|
566
|
+
// and event dispatch so the additive `errorInfo` wc-bindable property stays in
|
|
567
|
+
// sync with `error`. Each failure builds a fresh object (reference guard passes);
|
|
568
|
+
// the clear path passes null (suppresses a redundant null→null per share start).
|
|
569
|
+
_setErrorInfo(code, phase, recoverable, message, capabilityId) {
|
|
570
|
+
this._commitErrorInfo({ code, phase, recoverable, message, ...(capabilityId === undefined ? {} : { capabilityId }) });
|
|
571
|
+
}
|
|
572
|
+
_commitErrorInfo(info) {
|
|
573
|
+
if (this._errorInfo === info)
|
|
574
|
+
return;
|
|
575
|
+
this._errorInfo = info;
|
|
576
|
+
this._target.dispatchEvent(new CustomEvent("wcs-share:error-info-changed", {
|
|
577
|
+
detail: info,
|
|
578
|
+
bubbles: true,
|
|
579
|
+
}));
|
|
165
580
|
}
|
|
166
581
|
async share(data) {
|
|
167
|
-
// never-throw + unsupported (§8):
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
582
|
+
// never-throw + unsupported (§8 / §7.2): probe the required capability at call
|
|
583
|
+
// time. If `web.share` is absent, do NOT start — surface a stable
|
|
584
|
+
// `capability-missing` taxonomy and the existing error message shape.
|
|
585
|
+
const assessment = this.platformAssessment;
|
|
586
|
+
if (!requiredCapabilitiesAvailable(assessment, ShareCore.REQUIRED_CAPABILITIES)) {
|
|
587
|
+
const missing = ShareCore.REQUIRED_CAPABILITIES.find((id) => assessment.availability.get(id) !== "available");
|
|
588
|
+
const message = "Web Share API is not supported in this browser.";
|
|
589
|
+
this._setErrorInfo(WCS_SHARE_ERROR_CODE.CapabilityMissing, "start", false, message, missing);
|
|
590
|
+
this._setError({ message });
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
// exhaust: a share dialog is already open → reject this call as an idempotent
|
|
594
|
+
// no-op instead of racing a second navigator.share() (which would reject and
|
|
595
|
+
// corrupt the in-flight call's result). begin() returns null when active.
|
|
596
|
+
const started = this._lane.begin();
|
|
597
|
+
if (started === null) {
|
|
174
598
|
return null;
|
|
175
599
|
}
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
|
|
600
|
+
const { ticket } = started;
|
|
601
|
+
// Capability probed above → navigator.share is present. Resolve + bind at call
|
|
602
|
+
// time (never cached, §3.7) so tests can install/remove it freely.
|
|
603
|
+
const nav = globalThis.navigator;
|
|
604
|
+
const shareFn = nav.share.bind(nav);
|
|
605
|
+
// Start phase runs synchronously on a fresh ticket (no dispose can interleave
|
|
606
|
+
// before the first await), so these setters are unconditional. Post-await
|
|
607
|
+
// setters are gated by the lane's terminal CAS (claimTerminal), which fails on
|
|
608
|
+
// a stale/disposed ticket — share (exhaust, no supersede/timeout) needs no
|
|
609
|
+
// per-setter commit guard (unlike FetchCore's `latest` lane).
|
|
179
610
|
this._setLoading(true);
|
|
180
611
|
// Reset the previous outcome before starting a new share so a stale
|
|
181
|
-
// cancelled/error does not linger into this call's result
|
|
182
|
-
|
|
612
|
+
// cancelled/error/errorInfo does not linger into this call's result (§3).
|
|
613
|
+
this._commitErrorInfo(null);
|
|
183
614
|
this._setError(null);
|
|
184
615
|
this._setCancelled(false);
|
|
185
616
|
try {
|
|
186
617
|
await shareFn(data);
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
if (gen !== this._gen) {
|
|
618
|
+
// Terminal CAS: a stale (dispose-invalidated) completion loses the claim.
|
|
619
|
+
if (!this._lane.claimTerminal(ticket, "success")) {
|
|
190
620
|
return null;
|
|
191
621
|
}
|
|
192
|
-
// navigator.share() resolves `Promise<void>` — there is no payload to
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
// (docs/web-share-tag-design.md §4).
|
|
622
|
+
// navigator.share() resolves `Promise<void>` — there is no payload to read
|
|
623
|
+
// off the API, so `value` is synthesized as an echo of the caller's `data`,
|
|
624
|
+
// signalling "this share completed successfully" (§4).
|
|
196
625
|
this._setValue(data ?? null);
|
|
197
626
|
this._setLoading(false);
|
|
627
|
+
this._lane.finalize(ticket);
|
|
198
628
|
return data ?? null;
|
|
199
629
|
}
|
|
200
630
|
catch (e) {
|
|
201
|
-
|
|
202
|
-
if (
|
|
631
|
+
const cancelled = e?.name === "AbortError";
|
|
632
|
+
if (!this._lane.claimTerminal(ticket, cancelled ? "aborted" : "error")) {
|
|
203
633
|
return null;
|
|
204
634
|
}
|
|
205
|
-
if (
|
|
635
|
+
if (cancelled) {
|
|
206
636
|
// The user dismissed the share sheet — a routine cancellation, not a
|
|
207
|
-
// platform failure. Kept out of `error` (
|
|
637
|
+
// platform failure. Kept out of `error`/`errorInfo` (§3).
|
|
208
638
|
this._setCancelled(true);
|
|
209
639
|
}
|
|
210
640
|
else {
|
|
211
|
-
|
|
641
|
+
const message = String(e?.message ?? "Share failed.");
|
|
642
|
+
this._setErrorInfo(WCS_SHARE_ERROR_CODE.ShareFailed, "execute", true, message);
|
|
643
|
+
this._setError(e ?? { message });
|
|
212
644
|
}
|
|
213
645
|
this._setLoading(false);
|
|
646
|
+
this._lane.finalize(ticket);
|
|
214
647
|
return null;
|
|
215
648
|
}
|
|
216
649
|
}
|
|
@@ -308,6 +741,9 @@ class WcsShare extends HTMLElement {
|
|
|
308
741
|
get cancelled() {
|
|
309
742
|
return this._core.cancelled;
|
|
310
743
|
}
|
|
744
|
+
get errorInfo() {
|
|
745
|
+
return this._core.errorInfo;
|
|
746
|
+
}
|
|
311
747
|
get connectedCallbackPromise() {
|
|
312
748
|
return this._connectedCallbackPromise;
|
|
313
749
|
}
|
|
@@ -356,5 +792,5 @@ function bootstrapShare(userConfig) {
|
|
|
356
792
|
registerComponents();
|
|
357
793
|
}
|
|
358
794
|
|
|
359
|
-
export { ShareCore, WcsShare, bootstrapShare, getConfig };
|
|
795
|
+
export { ShareCore, WCS_SHARE_ERROR_CODE, WcsShare, bootstrapShare, getConfig };
|
|
360
796
|
//# sourceMappingURL=index.esm.js.map
|