weir-harness 2.0.0 → 2.1.1
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/lib/client.capability-badge.js +2 -0
- package/lib/client.capability-manager.js +46 -179
- package/lib/client.capability-model.js +428 -3
- package/lib/client.file-lease-tool-model.js +189 -0
- package/lib/client.file-lease-tool-view.js +434 -0
- package/lib/client.js +105 -7
- package/lib/client.mcp-manager.js +563 -0
- package/package.json +1 -1
- package/skills/mcp-server-setup/SKILL.md +124 -0
- package/src/capabilities/apply-engine.js +18 -4
- package/src/capabilities/capability-remote.js +296 -45
- package/src/capabilities/initial-selection.js +3 -2
- package/src/capabilities/lifecycle-preload.js +69 -11
- package/src/capabilities/lifecycle-snapshot.js +127 -39
- package/src/capabilities/mcp-admission.js +2 -2
- package/src/capabilities/mcp-config.js +344 -0
- package/src/capabilities/mcp-credentials.js +180 -0
- package/src/capabilities/mcp-diagnostics.js +169 -0
- package/src/capabilities/mcp-drain.js +18 -4
- package/src/capabilities/mcp-facade.js +40 -9
- package/src/capabilities/mcp-gate.js +51 -52
- package/src/capabilities/mcp-install-tool.js +276 -0
- package/src/capabilities/mcp-manager.js +575 -146
- package/src/capabilities/mcp-mount.js +220 -79
- package/src/capabilities/mcp-operations.js +359 -0
- package/src/capabilities/mcp-process.js +43 -0
- package/src/capabilities/mcp-registry.js +74 -60
- package/src/capabilities/mcp-runtime.js +103 -0
- package/src/capabilities/read-payloads.js +8 -3
- package/src/capabilities/skill-applicability.js +185 -0
- package/src/capabilities/skill-builtin-migration.js +2 -1
- package/src/capabilities/skill-selection-plugin.js +170 -57
- package/src/file-lease/client.js +268 -35
- package/src/file-lease/index.js +4 -0
- package/src/file-lease/plugin.js +239 -15
- package/src/file-lease/tools.js +426 -38
- package/src/file-lease/watch.js +473 -0
- package/src/intent-gate/index.js +29 -12
- package/src/intent-gate/matcher.js +36 -2
- package/src/lease-broker/protocol.js +3 -2
- package/src/lease-broker/server.js +6 -3
- package/src/lease-broker/state.js +68 -11
- package/src/process/index.js +4 -2
- package/src/shared/audit.js +3 -0
- package/src/shared/child-scope.js +2 -0
- package/src/worktree/rules.js +1 -1
package/src/file-lease/client.js
CHANGED
|
@@ -294,6 +294,13 @@ export function createLeaseClient({
|
|
|
294
294
|
/** @type {Promise<LeaseConnection> | null} */
|
|
295
295
|
let connecting = null
|
|
296
296
|
let closed = false
|
|
297
|
+
/**
|
|
298
|
+
* Waits that a close answers instead of the dial. `connect` has no abort
|
|
299
|
+
* contract — an injected one may never settle at all — so a close must
|
|
300
|
+
* release these itself; nothing here waits for a socket (D5).
|
|
301
|
+
* @type {Set<() => void>}
|
|
302
|
+
*/
|
|
303
|
+
const closeWaiters = new Set()
|
|
297
304
|
/** @type {number | undefined} */
|
|
298
305
|
let startedAt
|
|
299
306
|
/** A connection reset means the daemon may have restarted with an empty table (D6). */
|
|
@@ -306,6 +313,14 @@ export function createLeaseClient({
|
|
|
306
313
|
let lastDegradedAt = null
|
|
307
314
|
/** @type {string | undefined} */
|
|
308
315
|
let lastScope
|
|
316
|
+
/**
|
|
317
|
+
* The push events the CURRENT handshake advertised. Empty until a handshake
|
|
318
|
+
* answers and after the connection is gone, so "not advertised" and "not
|
|
319
|
+
* connected" both read as unsupported: a caller must never wait for an event
|
|
320
|
+
* this incarnation cannot send.
|
|
321
|
+
* @type {Set<string>}
|
|
322
|
+
*/
|
|
323
|
+
let eventCapabilities = new Set()
|
|
309
324
|
|
|
310
325
|
/**
|
|
311
326
|
* The replay ledger. `path -> {session, lineage, holdUntil}` — nothing more.
|
|
@@ -317,6 +332,10 @@ export function createLeaseClient({
|
|
|
317
332
|
const labels = new Map()
|
|
318
333
|
/** @type {Set<(event: any) => void>} */
|
|
319
334
|
const eventHandlers = new Set()
|
|
335
|
+
/** Connection-reset lifecycle listeners (D5). Separate from the wire events
|
|
336
|
+
* above on purpose: a reset is a property of THIS client's transport, not a
|
|
337
|
+
* fact the broker can send. @type {Set<() => void>} */
|
|
338
|
+
const resetHandlers = new Set()
|
|
320
339
|
/**
|
|
321
340
|
* @type {Map<string, {
|
|
322
341
|
* resolve: (reply: any) => void,
|
|
@@ -386,6 +405,24 @@ export function createLeaseClient({
|
|
|
386
405
|
}
|
|
387
406
|
}
|
|
388
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Tell every listener that the current connection is gone.
|
|
410
|
+
*
|
|
411
|
+
* Deliberately separate from wire events: a broker that is SIGKILLed cannot
|
|
412
|
+
* announce that it died, so a client that only listened to `EVENTS` would
|
|
413
|
+
* wait forever. Called exactly once per connection (the socket wrapper makes
|
|
414
|
+
* its close notification once-only) and never for an intentional `close()`.
|
|
415
|
+
*/
|
|
416
|
+
function notifyReset() {
|
|
417
|
+
for (const handler of [...resetHandlers]) {
|
|
418
|
+
try {
|
|
419
|
+
handler()
|
|
420
|
+
} catch {
|
|
421
|
+
// A listener is a consumer; it must never break the transport.
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
389
426
|
/** @param {string} line */
|
|
390
427
|
function handleFrame(line) {
|
|
391
428
|
/** @type {any} */
|
|
@@ -425,25 +462,61 @@ export function createLeaseClient({
|
|
|
425
462
|
}
|
|
426
463
|
}
|
|
427
464
|
|
|
428
|
-
/**
|
|
429
|
-
|
|
430
|
-
|
|
465
|
+
/**
|
|
466
|
+
* Answer every wait the close latch owns. A close is terminal for the whole
|
|
467
|
+
* transport lifetime, so a caller parked on a dial is told now rather than
|
|
468
|
+
* inheriting a dial that may never return.
|
|
469
|
+
*/
|
|
470
|
+
function notifyCloseWaiters() {
|
|
471
|
+
for (const waiter of [...closeWaiters]) waiter()
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* The ONE connection-invalidation outlet (D5).
|
|
476
|
+
*
|
|
477
|
+
* A connection ends in three ways — the socket closed under us, we dropped it
|
|
478
|
+
* to retry, or the client is closing — and all three must leave the same state
|
|
479
|
+
* behind: no current connection, no handshake, no advertised capabilities, no
|
|
480
|
+
* waiter still believing an answer is coming, and exactly one reset
|
|
481
|
+
* notification for the connection that just went away.
|
|
482
|
+
*
|
|
483
|
+
* Only the CURRENT target may do that. A stale target — a handshake that lost
|
|
484
|
+
* a race, or a socket already replaced by a retry — is a no-op: its late close
|
|
485
|
+
* must not clear the live connection's state or notify a second time.
|
|
486
|
+
*
|
|
487
|
+
* @param {LeaseConnection | null} target
|
|
488
|
+
* @param {boolean} [closing] true for an intentional client close: the ledger is
|
|
489
|
+
* already gone, so nothing is queued for replay
|
|
490
|
+
* @returns {boolean} whether this target was the current connection
|
|
491
|
+
*/
|
|
492
|
+
function invalidateConnection(target, closing = false) {
|
|
493
|
+
if (target === null || connection !== target) return false
|
|
431
494
|
connection = null
|
|
432
495
|
greeted = false
|
|
496
|
+
// The advertised capabilities belonged to that connection.
|
|
497
|
+
eventCapabilities = new Set()
|
|
433
498
|
// D6: a reset connection means the daemon may have died and come back with
|
|
434
499
|
// an empty table, so retained leases have to be replayed on the next dial.
|
|
435
|
-
if (ledger.size > 0) replayPending = true
|
|
436
|
-
failWaiters(transportError('lease broker connection closed'))
|
|
500
|
+
if (!closing && ledger.size > 0) replayPending = true
|
|
501
|
+
failWaiters(transportError(closing ? 'lease client closed' : 'lease broker connection closed'))
|
|
502
|
+
// D5: a reset is a terminal state for anything waiting on this connection,
|
|
503
|
+
// and the only moment a vanished broker can be reported at all.
|
|
504
|
+
notifyReset()
|
|
505
|
+
return true
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** @param {LeaseConnection} target */
|
|
509
|
+
function handleDisconnect(target) {
|
|
510
|
+
invalidateConnection(target)
|
|
437
511
|
}
|
|
438
512
|
|
|
439
513
|
/**
|
|
440
|
-
*
|
|
514
|
+
* Close a socket without touching client state. Used where the connection is
|
|
515
|
+
* already invalidated (or was never adopted) and only the transport is left.
|
|
516
|
+
* @param {LeaseConnection | null} target
|
|
441
517
|
*/
|
|
442
|
-
function
|
|
443
|
-
if (
|
|
444
|
-
connection = null
|
|
445
|
-
greeted = false
|
|
446
|
-
}
|
|
518
|
+
function closeSocket(target) {
|
|
519
|
+
if (target === null) return
|
|
447
520
|
try {
|
|
448
521
|
target.close()
|
|
449
522
|
} catch {
|
|
@@ -451,10 +524,80 @@ export function createLeaseClient({
|
|
|
451
524
|
}
|
|
452
525
|
}
|
|
453
526
|
|
|
454
|
-
/**
|
|
527
|
+
/**
|
|
528
|
+
* @param {LeaseConnection} target
|
|
529
|
+
* @returns {boolean} whether this target was the current connection
|
|
530
|
+
*/
|
|
531
|
+
function dropConnection(target) {
|
|
532
|
+
// Invalidate first: closing the socket reports its close synchronously, and
|
|
533
|
+
// that report must find nothing left to invalidate (exactly-once reset).
|
|
534
|
+
const invalidated = invalidateConnection(target)
|
|
535
|
+
closeSocket(target)
|
|
536
|
+
return invalidated
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Wait for `work`, but never past `close()`.
|
|
541
|
+
*
|
|
542
|
+
* The transport has no abort contract, so a close answers the wait itself:
|
|
543
|
+
* the dial is left to whatever it does, and nobody inherits a promise that
|
|
544
|
+
* may never settle. The waiter is removed as soon as either side answers, and
|
|
545
|
+
* `work` is always consumed — an abandoned dial that fails later can never
|
|
546
|
+
* surface as an unhandled rejection.
|
|
547
|
+
*
|
|
548
|
+
* @template T
|
|
549
|
+
* @param {Promise<T>} work
|
|
550
|
+
* @returns {Promise<T>}
|
|
551
|
+
*/
|
|
552
|
+
function untilClosed(work) {
|
|
553
|
+
return new Promise((resolve, reject) => {
|
|
554
|
+
let waiting = true
|
|
555
|
+
/** @param {() => void} settle */
|
|
556
|
+
const finish = (settle) => {
|
|
557
|
+
if (!waiting) return
|
|
558
|
+
waiting = false
|
|
559
|
+
closeWaiters.delete(onClose)
|
|
560
|
+
settle()
|
|
561
|
+
}
|
|
562
|
+
const onClose = () => finish(() => reject(transportError('lease client is closed')))
|
|
563
|
+
closeWaiters.add(onClose)
|
|
564
|
+
// Attach first: a close may already have fired, and `work`'s outcome must
|
|
565
|
+
// still be handled when this wait is answered by the latch instead.
|
|
566
|
+
Promise.resolve(work).then(
|
|
567
|
+
(value) => finish(() => resolve(value)),
|
|
568
|
+
(error) => finish(() => reject(error)),
|
|
569
|
+
)
|
|
570
|
+
if (closed) onClose()
|
|
571
|
+
})
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Dial once, but not past a close.
|
|
576
|
+
*
|
|
577
|
+
* A socket that only arrives after the close is closed on arrival: it can
|
|
578
|
+
* never be adopted, greeted, replayed or used to serve a request, and the
|
|
579
|
+
* caller was already answered by the latch.
|
|
580
|
+
* @returns {Promise<{ connection: LeaseConnection | null, error: any }>}
|
|
581
|
+
*/
|
|
455
582
|
async function attemptConnect() {
|
|
583
|
+
/** @type {LeaseConnection | Promise<LeaseConnection>} */
|
|
584
|
+
let started
|
|
585
|
+
try {
|
|
586
|
+
// Started synchronously: the dial is the caller's, not a later task's.
|
|
587
|
+
started = connect(endpoint)
|
|
588
|
+
} catch (error) {
|
|
589
|
+
return { connection: null, error }
|
|
590
|
+
}
|
|
591
|
+
/** @type {Promise<LeaseConnection>} */
|
|
592
|
+
const dial = Promise.resolve(started).then((connection) => {
|
|
593
|
+
if (closed) {
|
|
594
|
+
closeSocket(connection)
|
|
595
|
+
throw transportError('lease client is closed')
|
|
596
|
+
}
|
|
597
|
+
return connection
|
|
598
|
+
})
|
|
456
599
|
try {
|
|
457
|
-
return { connection: await
|
|
600
|
+
return { connection: await untilClosed(dial), error: null }
|
|
458
601
|
} catch (error) {
|
|
459
602
|
return { connection: null, error }
|
|
460
603
|
}
|
|
@@ -482,10 +625,21 @@ export function createLeaseClient({
|
|
|
482
625
|
* be able to start the daemon again on the next call, because "the daemon is
|
|
483
626
|
* gone" is a transient condition, not a decision to stop arbitrating.
|
|
484
627
|
*
|
|
628
|
+
* The wait is bounded by this client's lifetime, never by a dial timeout: a
|
|
629
|
+
* close answers it through the latch, and `connect` is left to settle on its
|
|
630
|
+
* own. A dial that never returns therefore cannot park a caller.
|
|
631
|
+
*
|
|
485
632
|
* @returns {Promise<LeaseConnection>}
|
|
486
633
|
*/
|
|
487
634
|
async function openConnection() {
|
|
635
|
+
// A close requested before or during the dial is terminal: never spawn,
|
|
636
|
+
// adopt or keep waiting for an arbiter on behalf of a client that is gone.
|
|
637
|
+
if (closed) throw transportError('lease client closed')
|
|
488
638
|
const first = await attemptConnect()
|
|
639
|
+
if (closed) {
|
|
640
|
+
closeSocket(first.connection)
|
|
641
|
+
throw transportError('lease client closed')
|
|
642
|
+
}
|
|
489
643
|
if (first.connection !== null) return first.connection
|
|
490
644
|
if (!isAbsentEndpoint(first.error)) {
|
|
491
645
|
throw transportError(`lease broker is unreachable at ${endpoint}`, first.error)
|
|
@@ -496,9 +650,15 @@ export function createLeaseClient({
|
|
|
496
650
|
// attempt count is what actually bounds the wait.
|
|
497
651
|
const maxAttempts = Math.max(1, Math.ceil(connectRetryBudgetMs / Math.max(1, connectRetryMs))) + 2
|
|
498
652
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
653
|
+
if (closed) throw transportError('lease client closed')
|
|
499
654
|
if (now() >= deadline) break
|
|
500
|
-
await delay(connectRetryMs)
|
|
655
|
+
await untilClosed(delay(connectRetryMs))
|
|
656
|
+
if (closed) throw transportError('lease client closed')
|
|
501
657
|
const retry = await attemptConnect()
|
|
658
|
+
if (closed) {
|
|
659
|
+
closeSocket(retry.connection)
|
|
660
|
+
throw transportError('lease client closed')
|
|
661
|
+
}
|
|
502
662
|
if (retry.connection !== null) return retry.connection
|
|
503
663
|
if (!isAbsentEndpoint(retry.error)) {
|
|
504
664
|
throw transportError(`lease broker is unreachable at ${endpoint}`, retry.error)
|
|
@@ -514,6 +674,12 @@ export function createLeaseClient({
|
|
|
514
674
|
*/
|
|
515
675
|
async function establish() {
|
|
516
676
|
const fresh = await openConnection()
|
|
677
|
+
// A close during the dial owns the socket now: it is closed instead of
|
|
678
|
+
// installed, so no handshake, replay or lease can happen after close.
|
|
679
|
+
if (closed) {
|
|
680
|
+
closeSocket(fresh)
|
|
681
|
+
throw transportError('lease client closed')
|
|
682
|
+
}
|
|
517
683
|
const previousStartedAt = startedAt
|
|
518
684
|
connection = fresh
|
|
519
685
|
greeted = false
|
|
@@ -537,6 +703,12 @@ export function createLeaseClient({
|
|
|
537
703
|
dropConnection(fresh)
|
|
538
704
|
throw transportError(`lease broker handshake failed at ${endpoint}`, error)
|
|
539
705
|
}
|
|
706
|
+
// A close may have invalidated this connection while the hello was in
|
|
707
|
+
// flight; even an already-delivered reply must not resurrect it.
|
|
708
|
+
if (closed) {
|
|
709
|
+
dropConnection(fresh)
|
|
710
|
+
throw transportError('lease client closed')
|
|
711
|
+
}
|
|
540
712
|
if (hello.ok !== true) {
|
|
541
713
|
// A protocol mismatch is infrastructure, not a conflict: the write goes
|
|
542
714
|
// through and the audit records why (D7). Nobody is asked to fix anything.
|
|
@@ -545,6 +717,11 @@ export function createLeaseClient({
|
|
|
545
717
|
throw transportError(`lease broker refused the handshake (${code})`)
|
|
546
718
|
}
|
|
547
719
|
greeted = true
|
|
720
|
+
// The handshake is the ONLY source of the push-event list, and an older
|
|
721
|
+
// broker simply does not carry the field: empty means "not advertised".
|
|
722
|
+
eventCapabilities = new Set(
|
|
723
|
+
Array.isArray(hello.events) ? hello.events.filter((entry) => typeof entry === 'string' && entry.length > 0) : [],
|
|
724
|
+
)
|
|
548
725
|
const reported = typeof hello.startedAt === 'number' ? hello.startedAt : previousStartedAt
|
|
549
726
|
startedAt = reported
|
|
550
727
|
const restarted = replayPending || (previousStartedAt !== undefined && previousStartedAt !== reported)
|
|
@@ -557,16 +734,26 @@ export function createLeaseClient({
|
|
|
557
734
|
}
|
|
558
735
|
|
|
559
736
|
/** @returns {Promise<LeaseConnection>} */
|
|
560
|
-
function ensureConnection() {
|
|
561
|
-
if (closed)
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
737
|
+
async function ensureConnection() {
|
|
738
|
+
if (closed) throw transportError('lease client is closed')
|
|
739
|
+
/** @type {LeaseConnection} */
|
|
740
|
+
let target
|
|
741
|
+
if (connection !== null && greeted) {
|
|
742
|
+
target = connection
|
|
743
|
+
} else if (connecting !== null) {
|
|
744
|
+
target = await connecting
|
|
745
|
+
} else {
|
|
746
|
+
/** @type {Promise<LeaseConnection>} */
|
|
747
|
+
const attempt = establish().finally(() => {
|
|
748
|
+
if (connecting === attempt) connecting = null
|
|
749
|
+
})
|
|
750
|
+
connecting = attempt
|
|
751
|
+
target = await attempt
|
|
752
|
+
}
|
|
753
|
+
// A close that landed while this dial was in flight is terminal: a caller
|
|
754
|
+
// resuming afterwards must never speak on the abandoned connection.
|
|
755
|
+
if (closed) throw transportError('lease client is closed')
|
|
756
|
+
return target
|
|
570
757
|
}
|
|
571
758
|
|
|
572
759
|
/**
|
|
@@ -578,11 +765,14 @@ export function createLeaseClient({
|
|
|
578
765
|
async function request(op, payload = {}) {
|
|
579
766
|
if (typeof op !== 'string' || op.length === 0) throw new TypeError('lease request requires an operation name')
|
|
580
767
|
const target = await ensureConnection()
|
|
768
|
+
if (closed) throw transportError('lease client is closed')
|
|
581
769
|
if (replayQueued) {
|
|
582
770
|
// Flush before the caller's own frame so an observe-after-reconnect call
|
|
583
771
|
// reports the world the replay has already restored.
|
|
584
772
|
replayQueued = false
|
|
585
773
|
await runReplay()
|
|
774
|
+
// The replay is wire I/O too: a close during it stops this request as well.
|
|
775
|
+
if (closed) throw transportError('lease client is closed')
|
|
586
776
|
}
|
|
587
777
|
return await exchange(target, op, payload)
|
|
588
778
|
}
|
|
@@ -932,6 +1122,7 @@ export function createLeaseClient({
|
|
|
932
1122
|
*/
|
|
933
1123
|
async function replayHolds() {
|
|
934
1124
|
await ensureConnection()
|
|
1125
|
+
if (closed) throw transportError('lease client is closed')
|
|
935
1126
|
// An explicit replay satisfies a queued one: never send the same retention
|
|
936
1127
|
// twice just because a reconnect happened to arm it.
|
|
937
1128
|
replayQueued = false
|
|
@@ -1014,7 +1205,7 @@ export function createLeaseClient({
|
|
|
1014
1205
|
}
|
|
1015
1206
|
|
|
1016
1207
|
/**
|
|
1017
|
-
* Subscribe to broker-pushed `request` / `settled` events.
|
|
1208
|
+
* Subscribe to broker-pushed `request` / `settled` / `released` events.
|
|
1018
1209
|
* @param {(event: any) => void} handler
|
|
1019
1210
|
* @returns {() => void} unsubscribe
|
|
1020
1211
|
*/
|
|
@@ -1027,24 +1218,64 @@ export function createLeaseClient({
|
|
|
1027
1218
|
}
|
|
1028
1219
|
|
|
1029
1220
|
/**
|
|
1030
|
-
*
|
|
1221
|
+
* Subscribe to connection resets (D5). A reset means the current connection
|
|
1222
|
+
* is gone, so anything that was waiting on a push from it is over — there is
|
|
1223
|
+
* nothing to replay and no fact to infer about ownership.
|
|
1224
|
+
* @param {() => void} handler
|
|
1225
|
+
* @returns {() => void} unsubscribe
|
|
1226
|
+
*/
|
|
1227
|
+
function onReset(handler) {
|
|
1228
|
+
if (typeof handler !== 'function') throw new TypeError('onReset requires a handler')
|
|
1229
|
+
resetHandlers.add(handler)
|
|
1230
|
+
return () => {
|
|
1231
|
+
resetHandlers.delete(handler)
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Read-only capability query: did the CURRENT handshake advertise this push
|
|
1237
|
+
* event? An older broker, a closed client and a dropped connection all answer
|
|
1238
|
+
* `false` — the caller must report the capability as unavailable instead of
|
|
1239
|
+
* waiting for an event that cannot arrive.
|
|
1240
|
+
* @param {string} name
|
|
1241
|
+
* @returns {boolean}
|
|
1242
|
+
*/
|
|
1243
|
+
function supportsEvent(name) {
|
|
1244
|
+
return typeof name === 'string' && eventCapabilities.has(name)
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Close the client. The daemon keeps running: it is shared with every other
|
|
1031
1249
|
* client on this machine, and its own idle window is what ends it.
|
|
1250
|
+
*
|
|
1251
|
+
* A close is terminal for the whole transport lifetime, including a dial or
|
|
1252
|
+
* handshake that is still in flight: the callers waiting on that attempt are
|
|
1253
|
+
* settled with a transport failure the moment the close lands (even when the
|
|
1254
|
+
* underlying dial never returns), the late socket is closed instead of
|
|
1255
|
+
* adopted, and any subscription waiting on it is told with exactly one reset
|
|
1256
|
+
* notification.
|
|
1032
1257
|
*/
|
|
1033
1258
|
function close() {
|
|
1034
1259
|
if (closed) return
|
|
1035
1260
|
closed = true
|
|
1036
|
-
const target = connection
|
|
1037
|
-
connection = null
|
|
1038
|
-
greeted = false
|
|
1039
1261
|
ledger.clear()
|
|
1040
|
-
|
|
1262
|
+
const target = connection
|
|
1041
1263
|
if (target !== null) {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1264
|
+
// An intentional close ends the current connection like any other loss, so
|
|
1265
|
+
// a subscription waiting on it is told instead of waiting forever.
|
|
1266
|
+
invalidateConnection(target, true)
|
|
1267
|
+
closeSocket(target)
|
|
1268
|
+
notifyCloseWaiters()
|
|
1269
|
+
return
|
|
1047
1270
|
}
|
|
1271
|
+
// No connection is installed yet, but a dial or handshake may still be in
|
|
1272
|
+
// flight. `establish` closes its late socket rather than adopting it and the
|
|
1273
|
+
// latch answers the callers now; the reset is reported here because no
|
|
1274
|
+
// connection exists to be invalidated later.
|
|
1275
|
+
const dialing = connecting !== null
|
|
1276
|
+
failWaiters(transportError('lease client closed'))
|
|
1277
|
+
if (dialing) notifyReset()
|
|
1278
|
+
notifyCloseWaiters()
|
|
1048
1279
|
}
|
|
1049
1280
|
|
|
1050
1281
|
return Object.freeze({
|
|
@@ -1061,6 +1292,8 @@ export function createLeaseClient({
|
|
|
1061
1292
|
releaseAll,
|
|
1062
1293
|
replayHolds,
|
|
1063
1294
|
onEvent,
|
|
1295
|
+
onReset,
|
|
1296
|
+
supportsEvent,
|
|
1064
1297
|
close,
|
|
1065
1298
|
})
|
|
1066
1299
|
}
|
package/src/file-lease/index.js
CHANGED
|
@@ -18,9 +18,13 @@ export { spawnDaemon } from './spawn.js'
|
|
|
18
18
|
export {
|
|
19
19
|
createLeaseTools,
|
|
20
20
|
REPLY_TOOL,
|
|
21
|
+
WATCH_TOOL,
|
|
21
22
|
MAX_HOLD_MINUTES,
|
|
22
23
|
DEFAULT_HOLD_MINUTES,
|
|
23
24
|
} from './tools.js'
|
|
25
|
+
// The in-memory release-subscription coordinator behind `edit_lock_watch` (D2):
|
|
26
|
+
// one per lease client, never persisted.
|
|
27
|
+
export { createWatchCoordinator, WATCH_STATES } from './watch.js'
|
|
24
28
|
export { createFileLeasePlugin, name, inject, apply } from './plugin.js'
|
|
25
29
|
// The read-only ownership projection behind the status surface (D13). Pure, so a
|
|
26
30
|
// caller can shape a snapshot without a client, a clock or a socket.
|