| @@ -519,17 +519,20 @@ export const createExecutor = < · 518 unmodified lines |
| 519 | return yield* provider.get(id); | 519 | return yield* provider.get(id); |
| 520 | } | 520 | } |
| 521 | | 521 | |
| 522 | // Fallback: ask enumerating providers in registration order. | 522 | // Fallback: ask every enumerating provider in parallel. First |
| 523 | // First non-null wins. Providers that throw are treated as | 523 | // non-null in registration order wins. Providers that throw |
| 524 | // "don't have it" and skipped so one flaky provider doesn't | 524 | // are treated as "don't have it" so one flaky provider can't |
| 525 | // block resolution via others. | 525 | // block resolution via others. |
| 526 | for (const provider of secretProviders.values()) { | 526 | const candidates = [...secretProviders.values()].filter( |
| 527 | if (!provider.list) continue; | 527 | (p) => p.list, |
| 528 | const value = yield* provider | 528 | ); |
| 529 | .get(id) | 529 | const values = yield* Effect.all( |
| 530 | .pipe(Effect.catchAll(() => Effect.succeed(null))); | 530 | candidates.map((p) => |
| 531 | if (value !== null) return value; | 531 | p.get(id).pipe(Effect.catchAll(() => Effect.succeed(null))), |
| 532 | } | 532 | ), |
| | 533 | { concurrency: "unbounded" }, |
| | 534 | ); |
| | 535 | for (const value of values) if (value !== null) return value; |
| 533 | return null; | 536 | return null; |
| 534 | }); | 537 | }); |
| 535 | | 538 | |
| @@ -595,11 +598,17 @@ export const createExecutor = < · 59 unmodified lines |
| 595 | | 598 | |
| 596 | const secretsRemove = (id: string): Effect.Effect<void, Error> => | 599 | const secretsRemove = (id: string): Effect.Effect<void, Error> => |
| 597 | Effect.gen(function* () { | 600 | Effect.gen(function* () { |
| 598 | for (const provider of secretProviders.values()) { | 601 | // Providers don't coordinate on which of them own the id — they |
| 599 | if (provider.writable && provider.delete) { | 602 | // each get asked. Most calls are no-ops; fan them out so one |
| 600 | yield* provider.delete(id); | 603 | // slow provider doesn't serialize the rest. |
| 601 | } | 604 | const deleters = [...secretProviders.values()].filter( |
| 602 | } | 605 | (p): p is typeof p & { delete: NonNullable<typeof p.delete> } => |
| | 606 | !!(p.writable && p.delete), |
| | 607 | ); |
| | 608 | yield* Effect.all( |
| | 609 | deleters.map((p) => p.delete(id)), |
| | 610 | { concurrency: "unbounded" }, |
| | 611 | ); |
| 603 | yield* core.delete({ | 612 | yield* core.delete({ |
| 604 | model: "secret", | 613 | model: "secret", |
| 605 | where: [{ field: "id", value: id }], | 614 | where: [{ field: "id", value: id }], |
| @@ -643,15 +652,26 @@ export const createExecutor = < · 37 unmodified lines |
| 643 | ); | 652 | ); |
| 644 | } | 653 | } |
| 645 | | 654 | |
| 646 | // Then every provider that can enumerate itself. If a provider | 655 | // Then every provider that can enumerate itself, in parallel. |
| 647 | // fails to list (unlocked vault, network error), swallow the | 656 | // If a provider fails to list (unlocked vault, network error), |
| 648 | // failure and continue — one flaky provider shouldn't block | 657 | // swallow the failure so one flaky provider can't block the |
| 649 | // the whole list. | 658 | // whole list. Merge in registration order afterwards so the |
| 650 | for (const [providerKey, provider] of secretProviders.entries()) { | 659 | // "first provider wins" precedence stays deterministic. |
| 651 | if (!provider.list) continue; | 660 | const listers = [...secretProviders.entries()].filter( |
| 652 | const entries = yield* provider | 661 | ([, p]) => p.list, |
| 653 | .list() | 662 | ); |
| 654 | .pipe(Effect.catchAll(() => Effect.succeed([] as const))); | 663 | const lists = yield* Effect.all( |
| | 664 | listers.map(([key, p]) => |
| | 665 | p |
| | 666 | .list!() |
| | 667 | .pipe( |
| | 668 | Effect.catchAll(() => Effect.succeed([] as const)), |
| | 669 | Effect.map((entries) => ({ key, entries })), |
| | 670 | ), |
| | 671 | ), |
| | 672 | { concurrency: "unbounded" }, |
| | 673 | ); |
| | 674 | for (const { key, entries } of lists) { |
| 655 | for (const entry of entries) { | 675 | for (const entry of entries) { |
| 656 | if (byId.has(entry.id)) continue; // core row wins | 676 | if (byId.has(entry.id)) continue; // core row wins |
| 657 | byId.set( | 677 | byId.set( |
| @@ -660,7 +680,7 @@ export const createExecutor = < · 2 unmodified lines |
| 660 | id: SecretId.make(entry.id), | 680 | id: SecretId.make(entry.id), |
| 661 | scopeId: scope.id, | 681 | scopeId: scope.id, |
| 662 | name: entry.name, | 682 | name: entry.name, |
| 663 | provider: providerKey, | 683 | provider: key, |
| 664 | createdAt: new Date(), | 684 | createdAt: new Date(), |
| 665 | }), | 685 | }), |
| 666 | ); | 686 | ); |