@cortexkit/antigravity-auth-core 3.0.29 → 3.0.30
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/dist/common-auth-embedded/claustrum/consumer.d.ts +12 -0
- package/dist/common-auth-embedded/claustrum/consumer.js +36 -0
- package/dist/common-auth-embedded/claustrum/custody.d.ts +16 -4
- package/dist/common-auth-embedded/claustrum/custody.js +74 -9
- package/dist/common-auth-embedded/claustrum/hub.d.ts +3 -2
- package/dist/common-auth-embedded/claustrum/hub.js +8 -0
- package/dist/common-auth-embedded/claustrum/index.d.ts +1 -0
- package/dist/common-auth-embedded/claustrum/roster.d.ts +2 -0
- package/dist/common-auth-embedded/claustrum/roster.js +2 -0
- package/dist/common-auth-embedded/claustrum/usage.d.ts +41 -0
- package/dist/common-auth-embedded/claustrum/usage.js +145 -0
- package/dist/common-auth-embedded/commands/builtins.d.ts +5 -0
- package/dist/common-auth-embedded/commands/builtins.js +38 -0
- package/dist/common-auth-embedded/fs/private-files.d.ts +5 -0
- package/dist/common-auth-embedded/fs/private-files.js +7 -0
- package/dist/common-auth-embedded/fs/refresh-file-lock.js +142 -47
- package/dist/common-auth-embedded/fs/with-lock.js +2 -5
- package/dist/common-auth-embedded/routing/admission.d.ts +7 -2
- package/dist/common-auth-embedded/routing/admission.js +17 -7
- package/dist/common-auth-embedded/source-output.json +126 -94
- package/dist/common-auth-embedded/store/attribution.js +3 -2
- package/dist/common-auth-embedded/store/errors.d.ts +1 -1
- package/dist/common-auth-embedded/store/hooks.d.ts +5 -1
- package/dist/common-auth-embedded/store/hooks.js +20 -5
- package/dist/common-auth-embedded/store/index.d.ts +1 -0
- package/dist/common-auth-embedded/store/limits-namespace.d.ts +7 -0
- package/dist/common-auth-embedded/store/limits-namespace.js +34 -0
- package/dist/common-auth-embedded/store/pool.d.ts +10 -1
- package/dist/common-auth-embedded/store/pool.js +2 -0
- package/dist/common-auth-embedded/store/provider-state.js +5 -5
- package/dist/common-auth-embedded/store/refresh.js +1 -1
- package/dist/common-auth-embedded/store/schema.js +9 -4
- package/dist/common-auth-embedded/store/settings.d.ts +2 -0
- package/dist/common-auth-embedded/store/settings.js +11 -4
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { lstat, mkdir, open, readFile, rename, rm, stat, } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
|
-
import { createPrivateBytes, createPrivateDirectoryExclusive, ensurePrivateDirectory, PrivateFileError, privateOperation, readPrivateText, verifyPrivate, windowsPrivateFiles, writePrivateAtomic, } from './private-files.js';
|
|
4
|
+
import { createPrivateBytes, createPrivateDirectoryExclusive, ensurePrivateDirectory, isWindowsSharingViolation, PrivateFileError, privateOperation, readPrivateText, verifyPrivate, windowsPrivateFiles, writePrivateAtomic, } from './private-files.js';
|
|
5
5
|
import { LockOwnershipError, lockPathFor } from './with-lock.js';
|
|
6
6
|
/**
|
|
7
7
|
* Create a new owner-only file and write `text` into it, failing with EEXIST if
|
|
@@ -367,54 +367,109 @@ export async function acquireRefreshFileLockWithClock(options) {
|
|
|
367
367
|
else
|
|
368
368
|
await rm(path, { recursive: true, force: true }).catch(() => { });
|
|
369
369
|
}
|
|
370
|
-
|
|
371
|
-
if (
|
|
372
|
-
|
|
370
|
+
function isRetryableRemoval(error, path) {
|
|
371
|
+
if (!windowsPrivateFiles ||
|
|
372
|
+
!(error instanceof PrivateFileError) ||
|
|
373
|
+
error.stage !== 'remove' ||
|
|
374
|
+
error.path !== path)
|
|
375
|
+
return false;
|
|
376
|
+
// Native sharing/delete-pending errors use isWindowsSharingViolation.
|
|
377
|
+
// node:fs rm supplies only errno: EBUSY/EPERM can also mean a sharing refusal.
|
|
378
|
+
return (isWindowsSharingViolation(error) ||
|
|
379
|
+
(error.win32Code === undefined &&
|
|
380
|
+
error.reason === undefined &&
|
|
381
|
+
(error.code === 'EBUSY' || error.code === 'EPERM')));
|
|
382
|
+
}
|
|
383
|
+
async function removeOwnedLeaseObject(path, stillOwned, deadline = performance.now() + EVICT_TTL + 1_000) {
|
|
384
|
+
let original;
|
|
385
|
+
let delayMs = 5;
|
|
386
|
+
while (await stillOwned()) {
|
|
387
|
+
try {
|
|
388
|
+
await removeLeaseObject(path);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
if (!isRetryableRemoval(error, path))
|
|
393
|
+
throw error;
|
|
394
|
+
original ??= error;
|
|
395
|
+
}
|
|
396
|
+
const remainingMs = deadline - performance.now();
|
|
397
|
+
if (remainingMs <= 0)
|
|
398
|
+
throw original;
|
|
399
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, remainingMs)));
|
|
400
|
+
if (performance.now() >= deadline)
|
|
401
|
+
throw original;
|
|
402
|
+
delayMs = Math.min(delayMs * 2, 50);
|
|
373
403
|
}
|
|
374
404
|
}
|
|
405
|
+
async function releaseEvictionMarker(deadline) {
|
|
406
|
+
let ownedDirectory;
|
|
407
|
+
await removeOwnedLeaseObject(evictPath, async () => {
|
|
408
|
+
if (!windowsPrivateFiles)
|
|
409
|
+
return ownsEvictionMarker();
|
|
410
|
+
const current = await lstat(evictPath).catch((error) => {
|
|
411
|
+
if (error.code !== 'ENOENT')
|
|
412
|
+
throw error;
|
|
413
|
+
return undefined;
|
|
414
|
+
});
|
|
415
|
+
if (!current ||
|
|
416
|
+
(ownedDirectory && !sameMarkerDirectory(ownedDirectory, current)))
|
|
417
|
+
return false;
|
|
418
|
+
if (await ownsEvictionMarker()) {
|
|
419
|
+
ownedDirectory = current;
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
if (!ownedDirectory)
|
|
423
|
+
return false;
|
|
424
|
+
// Recursive rm can delete owner.json before the directory delete fails.
|
|
425
|
+
// Only that same, now-ownerless directory is still ours to clean up.
|
|
426
|
+
try {
|
|
427
|
+
await stat(evictOwnerPath);
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
if (error.code !== 'ENOENT')
|
|
432
|
+
throw error;
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
}, deadline);
|
|
436
|
+
}
|
|
375
437
|
// Returns whether the directory at the marker path was still the one this
|
|
376
438
|
// process created when its owner write failed. Only then is the failure ours
|
|
377
439
|
// to report: a gone or replaced directory is a lost race.
|
|
378
|
-
async function removeOwnMarkerAfterFailedOwnerWrite(created) {
|
|
440
|
+
async function removeOwnMarkerAfterFailedOwnerWrite(created, deadline) {
|
|
379
441
|
// Best effort only: never throws, so the original owner-write error below
|
|
380
442
|
// keeps explaining the failure. Removes the directory this process just
|
|
381
443
|
// created when its owner write failed, and leaves a contender's
|
|
382
444
|
// replacement alone when the identities differ.
|
|
383
445
|
if (!created)
|
|
384
446
|
return false;
|
|
385
|
-
let
|
|
386
|
-
try {
|
|
387
|
-
const st = await lstat(evictPath);
|
|
388
|
-
current = { dev: st.dev, ino: st.ino, birthtimeMs: st.birthtimeMs };
|
|
389
|
-
}
|
|
390
|
-
catch {
|
|
391
|
-
// Renamed away by a contender's steal: nothing of ours left to clean.
|
|
392
|
-
return false;
|
|
393
|
-
}
|
|
394
|
-
if (!sameMarkerDirectory(created, current))
|
|
395
|
-
return false;
|
|
396
|
-
// Remove only an ownerless directory. An owner file that appeared after
|
|
397
|
-
// our failed write belongs to someone: ours goes through
|
|
398
|
-
// releaseEvictionMarker in the caller, and a contender's must stay, since
|
|
399
|
-
// it may be holding the marker right now.
|
|
447
|
+
let stillOurs = false;
|
|
400
448
|
try {
|
|
401
|
-
await
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
449
|
+
await removeOwnedLeaseObject(evictPath, async () => {
|
|
450
|
+
stillOurs = false;
|
|
451
|
+
const st = await lstat(evictPath).catch(() => undefined);
|
|
452
|
+
if (!st || !sameMarkerDirectory(created, st))
|
|
453
|
+
return false;
|
|
454
|
+
stillOurs = true;
|
|
455
|
+
// Recheck both identity and absence of an owner before every attempt;
|
|
456
|
+
// a contender's replacement or newly published owner must stay.
|
|
457
|
+
try {
|
|
458
|
+
await stat(evictOwnerPath);
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
return error.code === 'ENOENT';
|
|
463
|
+
}
|
|
464
|
+
}, deadline);
|
|
410
465
|
}
|
|
411
466
|
catch {
|
|
412
467
|
// A removal failure keeps the stale marker recoverable later; it must
|
|
413
468
|
// never mask the original owner-write error.
|
|
414
469
|
}
|
|
415
|
-
return
|
|
470
|
+
return stillOurs;
|
|
416
471
|
}
|
|
417
|
-
async function tryAcquireEvictionMarker() {
|
|
472
|
+
async function tryAcquireEvictionMarker(deadline) {
|
|
418
473
|
if (windowsPrivateFiles)
|
|
419
474
|
await createPrivateDirectoryExclusive(evictPath);
|
|
420
475
|
else
|
|
@@ -455,11 +510,11 @@ export async function acquireRefreshFileLockWithClock(options) {
|
|
|
455
510
|
// A race code while our own directory is still in place is not a lost
|
|
456
511
|
// race but a write that fails here every time: report it, rather than
|
|
457
512
|
// retrying the same failure until a deadline hides the cause.
|
|
458
|
-
const stillOurs = await removeOwnMarkerAfterFailedOwnerWrite(created);
|
|
513
|
+
const stillOurs = await removeOwnMarkerAfterFailedOwnerWrite(created, deadline);
|
|
459
514
|
if (isLostMarkerRaceError(error) && !stillOurs)
|
|
460
515
|
return false;
|
|
461
516
|
try {
|
|
462
|
-
await releaseEvictionMarker();
|
|
517
|
+
await releaseEvictionMarker(deadline);
|
|
463
518
|
}
|
|
464
519
|
catch {
|
|
465
520
|
// Removal must never mask the original owner-write error.
|
|
@@ -470,7 +525,7 @@ export async function acquireRefreshFileLockWithClock(options) {
|
|
|
470
525
|
await options.onStep('eviction-marker-acquired');
|
|
471
526
|
return true;
|
|
472
527
|
}
|
|
473
|
-
async function recoverStaleEvictionMarker() {
|
|
528
|
+
async function recoverStaleEvictionMarker(deadline) {
|
|
474
529
|
let evictStat;
|
|
475
530
|
try {
|
|
476
531
|
evictStat = await stat(evictPath);
|
|
@@ -529,12 +584,26 @@ export async function acquireRefreshFileLockWithClock(options) {
|
|
|
529
584
|
}
|
|
530
585
|
if (options.onStep)
|
|
531
586
|
await options.onStep('stale-marker-claimed');
|
|
532
|
-
|
|
587
|
+
const claimedIdentity = windowsPrivateFiles
|
|
588
|
+
? await lstat(claimedPath)
|
|
589
|
+
: undefined;
|
|
590
|
+
// The unique renamed claim is ours, not the current marker path where a
|
|
591
|
+
// successor may already be working.
|
|
592
|
+
await removeOwnedLeaseObject(claimedPath, async () => {
|
|
593
|
+
if (!claimedIdentity)
|
|
594
|
+
return true;
|
|
595
|
+
const current = await lstat(claimedPath).catch((error) => {
|
|
596
|
+
if (error.code !== 'ENOENT')
|
|
597
|
+
throw error;
|
|
598
|
+
return undefined;
|
|
599
|
+
});
|
|
600
|
+
return !!current && sameMarkerDirectory(claimedIdentity, current);
|
|
601
|
+
}, deadline);
|
|
533
602
|
return 'recovered';
|
|
534
603
|
}
|
|
535
|
-
async function withEvictionMarker(action) {
|
|
604
|
+
async function withEvictionMarker(action, deadline) {
|
|
536
605
|
try {
|
|
537
|
-
if (!(await tryAcquireEvictionMarker()))
|
|
606
|
+
if (!(await tryAcquireEvictionMarker(deadline)))
|
|
538
607
|
return false;
|
|
539
608
|
}
|
|
540
609
|
catch (error) {
|
|
@@ -555,12 +624,28 @@ export async function acquireRefreshFileLockWithClock(options) {
|
|
|
555
624
|
}
|
|
556
625
|
throw error;
|
|
557
626
|
}
|
|
627
|
+
let actionFailed = false;
|
|
628
|
+
let actionError;
|
|
558
629
|
try {
|
|
559
630
|
await action();
|
|
560
631
|
}
|
|
561
|
-
|
|
562
|
-
|
|
632
|
+
catch (error) {
|
|
633
|
+
actionFailed = true;
|
|
634
|
+
actionError = error;
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
await releaseEvictionMarker(deadline);
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
// Cleanup sharing failures must not replace the lock's original
|
|
641
|
+
// sharing refusal when both removals exhaust the same deadline.
|
|
642
|
+
if (isRetryableRemoval(actionError, lockPath) &&
|
|
643
|
+
isRetryableRemoval(error, evictPath))
|
|
644
|
+
throw actionError;
|
|
645
|
+
throw error;
|
|
563
646
|
}
|
|
647
|
+
if (actionFailed)
|
|
648
|
+
throw actionError;
|
|
564
649
|
return true;
|
|
565
650
|
}
|
|
566
651
|
// Marker loss after a write may mean another contender has taken over.
|
|
@@ -847,6 +932,7 @@ export async function acquireRefreshFileLockWithClock(options) {
|
|
|
847
932
|
// wall-clock adjustments cannot extend the retry deadline.
|
|
848
933
|
const deadline = performance.now() + EVICT_TTL + 1_000;
|
|
849
934
|
let delayMs = 5;
|
|
935
|
+
let removalError;
|
|
850
936
|
for (;;) {
|
|
851
937
|
try {
|
|
852
938
|
let removalConfirmed = false;
|
|
@@ -873,7 +959,7 @@ export async function acquireRefreshFileLockWithClock(options) {
|
|
|
873
959
|
return;
|
|
874
960
|
await removeLeaseObject(lockPath);
|
|
875
961
|
removalConfirmed = true;
|
|
876
|
-
});
|
|
962
|
+
}, deadline);
|
|
877
963
|
if (markerAcquired) {
|
|
878
964
|
if (windowsPrivateFiles && !removalConfirmed)
|
|
879
965
|
throw new PrivateFileError(lockPath, 'release-not-confirmed', {
|
|
@@ -884,25 +970,34 @@ export async function acquireRefreshFileLockWithClock(options) {
|
|
|
884
970
|
}
|
|
885
971
|
if (signal?.aborted)
|
|
886
972
|
return;
|
|
887
|
-
await recoverStaleEvictionMarker();
|
|
973
|
+
await recoverStaleEvictionMarker(deadline);
|
|
888
974
|
}
|
|
889
975
|
catch (error) {
|
|
890
|
-
if (
|
|
891
|
-
|
|
892
|
-
|
|
976
|
+
if (!isRetryableRemoval(error, lockPath)) {
|
|
977
|
+
if (windowsPrivateFiles)
|
|
978
|
+
throw error;
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
// Like rename, a fenced delete can be refused by a reader's Windows
|
|
982
|
+
// handle without invalidating ownership. Drop the marker, back off,
|
|
983
|
+
// and re-confirm the owner under a new marker before trying again.
|
|
984
|
+
removalError ??= error;
|
|
893
985
|
}
|
|
894
986
|
const remainingMs = deadline - performance.now();
|
|
895
987
|
if (remainingMs <= 0)
|
|
896
988
|
break;
|
|
897
989
|
await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, remainingMs)));
|
|
990
|
+
if (windowsPrivateFiles && performance.now() >= deadline)
|
|
991
|
+
break;
|
|
898
992
|
delayMs = Math.min(delayMs * 2, 50);
|
|
899
993
|
}
|
|
900
994
|
// Do not delete by pathname without the marker: bounded retries leave the
|
|
901
995
|
// lease to expire rather than risking removal of a successor's lock.
|
|
902
996
|
if (windowsPrivateFiles)
|
|
903
|
-
throw
|
|
904
|
-
|
|
905
|
-
|
|
997
|
+
throw (removalError ??
|
|
998
|
+
new PrivateFileError(lockPath, 'release-not-confirmed', {
|
|
999
|
+
code: 'ETIMEDOUT',
|
|
1000
|
+
}));
|
|
906
1001
|
},
|
|
907
1002
|
};
|
|
908
1003
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join, resolve } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { isWindowsSharingViolation, } from './private-files.js';
|
|
3
3
|
import { acquireRefreshFileLock } from './refresh-file-lock.js';
|
|
4
4
|
export function lockPathFor(target, name) {
|
|
5
5
|
return `${target}.${name}.lock`;
|
|
@@ -21,10 +21,7 @@ export class LockOwnershipError extends Error {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
function isChangingLockRead(error, target, name) {
|
|
24
|
-
if (!
|
|
25
|
-
!(error instanceof PrivateFileError) ||
|
|
26
|
-
error.reason !== 'io-error' ||
|
|
27
|
-
(error.win32Code !== 32 && error.win32Code !== 303) ||
|
|
24
|
+
if (!isWindowsSharingViolation(error) ||
|
|
28
25
|
(error.stage !== 'read' && error.stage !== 'verify'))
|
|
29
26
|
return false;
|
|
30
27
|
const path = resolve(lockPathFor(target, name));
|
|
@@ -23,11 +23,16 @@ export interface AdmissionInput extends ExclusionInputs {
|
|
|
23
23
|
scope?: string;
|
|
24
24
|
/** Labels the scope requires an entry for; defaults to `['primary']`. */
|
|
25
25
|
requiredLabels?: readonly string[];
|
|
26
|
+
/**
|
|
27
|
+
* One-dispatch exemptions keyed by row; waive only gate 4. The caller first
|
|
28
|
+
* validates the current account incarnation and checkpointed reset times.
|
|
29
|
+
*/
|
|
30
|
+
liftedWindows?: ReadonlyMap<string, readonly WindowRef[]>;
|
|
26
31
|
now: number;
|
|
27
32
|
/**
|
|
28
|
-
* Called synchronously
|
|
33
|
+
* Called synchronously once per row needing a pull, including lifted windows.
|
|
29
34
|
* Its return value is ignored and never awaited, so a pull the caller
|
|
30
|
-
* starts from here cannot delay
|
|
35
|
+
* starts from here cannot delay admission or a refusal.
|
|
31
36
|
*/
|
|
32
37
|
requestPull?: (rowId: string) => void;
|
|
33
38
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// 3. a required label with no entry is unknown: refused, pull requested;
|
|
15
15
|
// 4. a reading at or beyond 100% whose reset is missing, unparsable or not
|
|
16
16
|
// after `now` cannot confirm the exhaustion, so the window is unknown:
|
|
17
|
-
// refused, pull requested;
|
|
17
|
+
// refused unless lifted for this wake dispatch; either way, pull requested;
|
|
18
18
|
// 5. a reading at or beyond 100% with a future reset is exhausted: refused.
|
|
19
19
|
// Gates 4 and 5 look at every projected limit, not only the required labels.
|
|
20
20
|
// Stage 2 judges the credit budget across the stage-1 survivors: a row whose
|
|
@@ -36,7 +36,7 @@ export function exclusionFor(id, inputs, now) {
|
|
|
36
36
|
}
|
|
37
37
|
return undefined;
|
|
38
38
|
}
|
|
39
|
-
function judgeStageOne(id, projection, requiredLabels, now) {
|
|
39
|
+
function judgeStageOne(id, projection, requiredLabels, now, liftedWindows) {
|
|
40
40
|
if (projection.limits.length === 0) {
|
|
41
41
|
return {
|
|
42
42
|
id,
|
|
@@ -60,7 +60,8 @@ function judgeStageOne(id, projection, requiredLabels, now) {
|
|
|
60
60
|
}
|
|
61
61
|
const exhausted = projection.limits.filter(readsExhausted);
|
|
62
62
|
for (const limit of exhausted) {
|
|
63
|
-
if (futureResetAt(limit, now) === undefined
|
|
63
|
+
if (futureResetAt(limit, now) === undefined &&
|
|
64
|
+
!liftedWindows.some((window) => window.scope === limit.scope && window.label === limit.label)) {
|
|
64
65
|
return {
|
|
65
66
|
id,
|
|
66
67
|
stage: 1,
|
|
@@ -94,6 +95,7 @@ export function admit(input) {
|
|
|
94
95
|
const excluded = [];
|
|
95
96
|
const refused = [];
|
|
96
97
|
const survivors = [];
|
|
98
|
+
const needsPull = new Set();
|
|
97
99
|
for (const row of input.rows) {
|
|
98
100
|
const exclusion = exclusionFor(row.id, input, input.now);
|
|
99
101
|
if (exclusion) {
|
|
@@ -105,7 +107,12 @@ export function admit(input) {
|
|
|
105
107
|
continue;
|
|
106
108
|
}
|
|
107
109
|
const projection = projectQuota(row.quota, scope);
|
|
108
|
-
const
|
|
110
|
+
const lifted = input.liftedWindows?.get(row.id) ?? [];
|
|
111
|
+
if (projection.limits.some((limit) => readsExhausted(limit) &&
|
|
112
|
+
futureResetAt(limit, input.now) === undefined &&
|
|
113
|
+
lifted.some((window) => window.scope === limit.scope && window.label === limit.label)))
|
|
114
|
+
needsPull.add(row.id);
|
|
115
|
+
const refusal = judgeStageOne(row.id, projection, requiredLabels, input.now, lifted);
|
|
109
116
|
if (refusal) {
|
|
110
117
|
refused.push(refusal);
|
|
111
118
|
continue;
|
|
@@ -133,9 +140,12 @@ export function admit(input) {
|
|
|
133
140
|
const order = new Map(input.rows.map((row, index) => [row.id, index]));
|
|
134
141
|
const byInput = (left, right) => (order.get(left.id) ?? 0) - (order.get(right.id) ?? 0);
|
|
135
142
|
refused.sort(byInput);
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
143
|
+
for (const refusal of refused)
|
|
144
|
+
if ('pullRequested' in refusal)
|
|
145
|
+
needsPull.add(refusal.id);
|
|
146
|
+
const pulls = input.rows
|
|
147
|
+
.filter((row) => needsPull.has(row.id))
|
|
148
|
+
.map((row) => row.id);
|
|
139
149
|
for (const id of pulls)
|
|
140
150
|
input.requestPull?.(id);
|
|
141
151
|
return { admitted, refused, excluded, pulls };
|