Skip to content

Commit 47226e6

Browse files
feat(spec): IOwnershipQueueService lets a caller ask for a refusal on a failed member read (onFailedRead) (#30318)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBLWzy6uXpPjBQbCmtm2Mgpz)_ domain:spec#3 · os-support-ai · session_01WGufzfHeHezsXgrF5MEs4N Fixes #30251 Part of #29604 (this PR is the contract only; the producer in `plugin-sharing` and the D6/D7 callers that pass `'refuse'` stay with #29604's code half, part 2) Clause-②: yes (widening: an optional `onFailedRead` on `expandMembers`' options and an optional options argument on `isMember`; precedent PR #30015) Metadata-protocol: none Skills-docs: none — a call-time option of a kernel service contract that no metadata author writes; `skills/**` and `content/docs/**` name the ownership-queue service but none of its methods (`git grep -n -i "expandMembers|IOwnershipQueueService" -- skills content/docs`: 0 hits) Prior rulings read: onfailedread,ownership queue,expandmembers → 0 hits; none; thread: none; repo: objectstack-ai/objectstack ## 维护者速读 - 改了什么:队列成员服务的契约(`IOwnershipQueueService`)新增一个可选开关 `onFailedRead`,调用方可以说明「成员表读失败时」要的是「当作没人」还是「直接报错」。只改类型和文档注释,不改任何运行逻辑。 - 为什么改:ADR-0142 D2 规定审批人名单(D6)和共享授权重算(D7)遇到读失败必须报错,否则一次数据库故障就会撤掉整个队列给出的授权;但审批插件只能通过这份契约调用队列,原来没有办法表达这个要求。 - 风险与回滚:默认值就是今天的行为(`'nobody'`),所有现有调用和实现原样编译、原样运行;回滚即还原本 PR。真正改行为的是 #29604 的第二部分。 - 先看:`packages/spec/src/contracts/ownership-queue-service.ts`(新类型 `OwnershipQueueFailedReadAnswer` 的注释和两个方法签名)、`ownership-queue-failed-read.pin.test.ts`。 - 两分钟验证:`pnpm --filter @objectstack/spec check:test-typecheck`,预期 OK;pin 文件里四个 `@ts-expect-error` 证明非法取值编译不过。 - 需要你做的:无,席位落地。 ## What changes - **New exported type `OwnershipQueueFailedReadAnswer = 'nobody' | 'refuse'`** (`@objectstack/spec/contracts`). Its docblock carries the whole rule, citing ADR-0142, D2 "Membership", the amended paragraph "An empty expansion is a fact; a failed read is a fault": - the tables behind a queue: `sys_queue`, `sys_queue_member`, `sys_team_member`, `sys_user_position`, `sys_member` through the position transition limb, and the business-unit membership and tree (D2 names the BU reads too, so the list keeps them); a table the composition does not carry is absence, not failure; - `'nobody'`: the default and today's answer, logged at `warn`; taken by the access path (D3), the claim admission (D4) and the assignment pool (D5); - `'refuse'`: a failed read rejects with a failure that names the read, never `[]`, a partial set or `false`; taken by the approver slate (D6) and the sharing-recipient grant recompute (D7); - under either answer an empty queue is still `[]`, and a name that resolves nowhere is still `[]` / `false`; - ⛔ independent of `QueueExpansionPurpose` (contract review 6600699486, carried in 6600766777). - **`expandMembers(name, organizationId, options?: { purpose?; onFailedRead? })`**: the new key sits beside `purpose`; the method docblock points at the type. - **`isMember(userId, name, organizationId, options?: { onFailedRead? })`**: a new optional trailing argument, so every three-argument call compiles unchanged. Under `'refuse'`, `false` is answered only when every read behind it succeeded. - **Module header**: one sentence, "A read that FAILS is not absence", pointing at the new type. - **Generated**: `api-surface/contracts.json` and `export-origins/contracts.json` gain the one new type (`check:generated --fix`, rebuilt dist). - **Changeset**: `@objectstack/spec` `patch` (`Metadata-protocol: none` grades every entry `patch`). ## Caller census `git grep -n "IOwnershipQueueService\|expandMembers(\|isMember("` at `959c26be`, every hit outside generated JSON and the contract itself: | site | what it uses | effect of the widening | |---|---|---| | `plugin-sharing/src/ownership-queue-service.ts:127` `OwnershipQueueService implements IOwnershipQueueService` | the one implementer; `expandMembers(…, options?: { purpose? })`, `isMember(userId, name, org)` | still assignable (method parameter bivariance; an implementation may take fewer trailing params). Type-checked, see Tests. Behaviour unchanged: it ignores the key until #29604 part 2 | | `plugin-sharing/src/sharing-rule-service.ts:432, :1747, :2186` (D7) | `Pick` of 'expandMembers' \| 'isMember', calls without options | compiles unchanged; these are the call sites #29604 part 2 moves to `'refuse'` | | `plugin-sharing/src/sharing-service.ts:557, :952` (D3) | `Pick` of 'queuesOf' \| 'isMember', 3-arg `isMember` | unchanged, default `'nobody'` | | `plugin-sharing/src/index.ts:177` | type re-export | unchanged | | `plugin-approvals/src/approval-service.ts:1638, :3347` (D6) | `Pick` of 'expandMembers', `{ purpose: 'addressing' }` | compiles unchanged; #29604 part 2 adds `onFailedRead: 'refuse'` here | | `objectql/src/assignment-rules.ts:74, :293` (D5) | `Pick` of 'resolveQueue' \| 'expandMembers', 2-arg call | unchanged, default `'nobody'` | | `plugin-security/src/ownership-queue-gate.ts:32`, `security-plugin.ts:2670`, `security-plugin-authz-middleware.ts:1602` (D4), `security-plugin-explain-access.ts:253, :512` | `Pick` of 'resolveQueue' \| 'isMember', 3-arg `isMember` | unchanged | | `plugin-security/src/kernel-queue-membership.ts:44`, `security-plugin.ts:2683` | `Pick` of 'queuesOf' | member untouched | | `plugin-hono-server/src/current-user-endpoints.ts:317` | `Pick` of 'hasAnyQueue' | member untouched | | `runtime/src/domains/catalog-activation.ts:166` | `Pick` of 'declaredQueueNamed' | member untouched | | tests: `plugin-sharing/src/ownership-queue.test.ts`, `plugin-security/src/kernel-queue-membership.test.ts:113` | calls without options | compile unchanged | No caller breaks: the key is optional on an options bag that was already optional, and the `isMember` argument is a new optional trailing one. ## Tests - New `packages/spec/src/contracts/ownership-queue-failed-read.pin.test.ts`, compiled by `check:test-typecheck` under `tsconfig.test.json`: - type-level `Assert`/`Eq` pins: the answer type is exactly `'nobody' | 'refuse'`; both members' `onFailedRead` read it; `expandMembers`' option keys are exactly `purpose | onFailedRead` and `isMember`'s exactly `onFailedRead`; `purpose` keeps its type; the two value sets are disjoint; - an implementation written before the key (`expandMembers` with `{ purpose? }` only, a 3-param `isMember`) is still an `IOwnershipQueueService`; - call shapes that compile: no options, `{}`, `{ purpose }`, `{ onFailedRead: 'refuse' }`, both keys, the 3-arg and 4-arg `isMember`; - four `@ts-expect-error` shapes that must not compile: `onFailedRead: 'throw'`, `'empty'`, `'addressing'`, and `purpose` on `isMember`; - one runtime case, so vitest counts the file. <details><summary>Reverse verification (two mutations, each restored by scripts/ablation-replace.mjs to the HEAD blob)</summary> - Mutation 1: delete `isMember`'s `options?: { onFailedRead?: OwnershipQueueFailedReadAnswer },` → `check:test-typecheck` red, `ownership-queue-failed-read.pin.test.ts: 4 type error(s)`; restored, blob `2bd06d9682c3` == HEAD, `git diff HEAD` empty. - Mutation 2: widen the type to `'nobody' | 'refuse' | 'throw'` → red, `2 type error(s)` (the `Eq` pin and the now-unused `@ts-expect-error`); restored, blob == HEAD. Both read through `tsconfig.test.json`, which resolves the contract from `src`, so no dist rebuild was needed for the legs. </details> Local runs, all on head `12fb7d30`, each under `os-verify-lock` with `NODE_OPTIONS=--max-old-space-size=4096`: - `pnpm --filter @objectstack/spec typecheck` (tsc, scripts, `check:test-typecheck`) and the new pin file: VERDICT command-exit 0; `Test Files 1 passed (1)`. - `pnpm --filter @objectstack/spec test`: VERDICT command-exit 0; `Test Files 813 passed (813)`, `Tests 25576 passed | 1 todo (25577)`. - Downstream (the contract's import side, `^...` closure built first, 36 turbo tasks): `typecheck` of `@objectstack/plugin-sharing` (the implementer), `@objectstack/plugin-approvals`, `@objectstack/plugin-security` and `@objectstack/objectql`: VERDICT command-exit 0, each `check:test-typecheck: OK`. `plugin-hono-server` and `runtime` pick only `hasAnyQueue` / `declaredQueueNamed`, members this PR does not touch, so they were not re-typechecked (CI's full typecheck covers them). - `check:generated --fix` rebuilt `packages/spec/dist` and regenerated `api-surface/` and `export-origins/` (the two artifacts it proved stale); a later plain `check:generated` is green. ## Acceptance notes - The activation-ledger read (`switchedOff`, answered "on" when its read fails) is not named in the new docblock: contract review 6600766777 leaves that posture for #29604's code half to decide and say. - `queuesOf` gets no `onFailedRead`: no ADR-0142 recompute consumer reads it, and the card does not ask for it. ## Verification - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 80 families at `12fb7d30`; all 80 ran, every one exit 0. `--ran` reconciliation: `80 derived famil(ies) accounted for — 80 run, 0 NOT-MEASURED`. - Three first-pass non-zero exits, each a local prerequisite and re-run green on the same head: - `check-plugin-teardown-shape.mjs --self-test` exit 3: shallow clone missing the pinned fixture commit; fetched it, then exit 0 (48 cases pass); - `spec check:generated` exit 1: the runner had no `NODE_OPTIONS`, and `check:test-typecheck`'s tsc ran out of the default 2 GB heap; re-run with 4 GB, exit 0; - `check:dual-build-cjs-loads` exit 3 (no dist for 38 packages); after a full `turbo run build` (120 tasks), exit 0: 116 require entry points across 71 packages load. - `check-changeset-no-major.mjs --base origin/main` exit 0 with the `@objectstack/spec` `patch` entry under `Metadata-protocol: none`. - Repo-wide lint and `test:repo` are CI's runs. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (a TypeScript contract type only) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (repo-wide lint is CI's run) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. (the maintainer brief is in Chinese by repo rule) - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WGufzfHeHezsXgrF5MEs4N --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0837fdc commit 47226e6

5 files changed

Lines changed: 165 additions & 3 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
feat(spec): `IOwnershipQueueService` lets a caller ask for a refusal on a failed member read (`onFailedRead`), the call-time signal ADR-0142 D2 asks each consumer to send
6+
7+
Clause-②: yes (widening: an optional `onFailedRead` on `expandMembers`' options and an optional options argument on `isMember`; precedent PR #30015)
8+
Metadata-protocol: none
9+
10+
- **New type `OwnershipQueueFailedReadAnswer`** (`'nobody' | 'refuse'`, `@objectstack/spec/contracts`): what a membership read answers when a read behind the queue fails (`sys_queue`, `sys_queue_member`, `sys_team_member`, `sys_user_position`, `sys_member` through the position transition limb, the business-unit membership and tree).
11+
- **`expandMembers(name, organizationId, { purpose?, onFailedRead? })`** and **`isMember(userId, name, organizationId, { onFailedRead? })`**. `'nobody'` is the default and the answer every caller had before: a failed read is logged at `warn` and contributes nobody. Under `'refuse'` a failed read rejects with a failure that names the read, never `[]`, a partial set or `false`; an empty queue is still `[]`. The docblock names who takes which: the access path (D3), the claim admission (D4) and the assignment pool (D5) take `'nobody'`; the approver slate (D6) and the sharing-recipient grant recompute (D7) take `'refuse'`. The key is independent of `purpose`.
12+
- **Contract only.** Every existing call and implementation compiles unchanged and behaves as today; the producer in `@objectstack/plugin-sharing` and the D6/D7 callers that pass `'refuse'` land with #29604.

‎packages/spec/api-surface/contracts.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@
241241
"OWNERSHIP_QUEUE_SERVICE (const)",
242242
"ObjectDraft (interface)",
243243
"OwnershipFloorAlternate (interface)",
244+
"OwnershipQueueFailedReadAnswer (type)",
244245
"PRINCIPAL_SESSION_MAX_TTL_SECONDS (const)",
245246
"PendingActionRow (interface)",
246247
"PendingActionStatus (type)",

‎packages/spec/export-origins/contracts.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@
241241
"OWNERSHIP_QUEUE_SERVICE": "src/contracts/ownership-queue-service.ts#OWNERSHIP_QUEUE_SERVICE (const)",
242242
"ObjectDraft": "src/contracts/external-datasource-service.ts#ObjectDraft (interface)",
243243
"OwnershipFloorAlternate": "src/contracts/security-service.ts#OwnershipFloorAlternate (interface)",
244+
"OwnershipQueueFailedReadAnswer": "src/contracts/ownership-queue-service.ts#OwnershipQueueFailedReadAnswer (type)",
244245
"PRINCIPAL_SESSION_MAX_TTL_SECONDS": "src/contracts/auth-service.ts#PRINCIPAL_SESSION_MAX_TTL_SECONDS (const)",
245246
"PendingActionRow": "src/contracts/ai-service.ts#PendingActionRow (interface)",
246247
"PendingActionStatus": "src/contracts/ai-service.ts#PendingActionStatus (type)",
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#30251] `onFailedRead` on {@link IOwnershipQueueService}: the call-time
5+
* signal ADR-0142 D2 (as amended) asks each consumer to send, so a recompute
6+
* consumer reached only through this contract (the D6 approver slate in
7+
* `@objectstack/plugin-approvals`) can ask for a refusal on a failed read.
8+
*
9+
* Pinned at the type level, under `check:test-typecheck` (this file compiles
10+
* under `tsconfig.test.json`):
11+
*
12+
* 1. **A widening, not a narrowing.** A call with no options, and the
13+
* three-argument `isMember` call, compile exactly as before; so does an
14+
* implementation written before the key existed.
15+
* 2. **Both members read one value type.** `expandMembers`' and `isMember`'s
16+
* `onFailedRead` are both {@link OwnershipQueueFailedReadAnswer}, which is
17+
* exactly `'nobody' | 'refuse'`; an unknown value does not compile.
18+
* 3. **Independent of `purpose`.** The two keys sit side by side; neither
19+
* value set leaks into the other.
20+
*
21+
* ⛔ Not pinned: what any implementation DOES under `'refuse'` — that is the
22+
* producer half (#29604, `@objectstack/plugin-sharing`).
23+
*/
24+
25+
import { describe, it, expect } from 'vitest';
26+
27+
import type {
28+
IOwnershipQueueService,
29+
OwnershipQueueFailedReadAnswer,
30+
QueueExpansionPurpose,
31+
} from './ownership-queue-service';
32+
33+
/** Type-level identity: true iff A and B are the same type. */
34+
type Eq<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
35+
/** Compile error when the argument is not `true`. */
36+
type Assert<T extends true> = T;
37+
38+
type ExpandOptions = NonNullable<Parameters<IOwnershipQueueService['expandMembers']>[2]>;
39+
type IsMemberOptions = NonNullable<Parameters<IOwnershipQueueService['isMember']>[3]>;
40+
41+
export type _AnswerIsExactlyTwoValues = Assert<Eq<OwnershipQueueFailedReadAnswer, 'nobody' | 'refuse'>>;
42+
export type _ExpandReadsTheAnswerType = Assert<Eq<ExpandOptions['onFailedRead'], OwnershipQueueFailedReadAnswer | undefined>>;
43+
export type _IsMemberReadsTheAnswerType = Assert<Eq<IsMemberOptions['onFailedRead'], OwnershipQueueFailedReadAnswer | undefined>>;
44+
export type _PurposeUnchanged = Assert<Eq<ExpandOptions['purpose'], QueueExpansionPurpose | undefined>>;
45+
export type _ExpandOptionKeys = Assert<Eq<keyof ExpandOptions, 'purpose' | 'onFailedRead'>>;
46+
export type _IsMemberOptionKeys = Assert<Eq<keyof IsMemberOptions, 'onFailedRead'>>;
47+
/** The axes stay apart: no failed-read answer is a purpose, and no purpose is a failed-read answer. */
48+
export type _AxesDisjoint = Assert<Eq<Extract<OwnershipQueueFailedReadAnswer, QueueExpansionPurpose>, never>>;
49+
50+
/** An implementation written before the key existed still satisfies the contract (method bivariance on an optional trailing argument). */
51+
const legacy: IOwnershipQueueService = {
52+
resolveQueue: async () => null,
53+
expandMembers: async (_name: string, _org: string | null, _options?: { purpose?: QueueExpansionPurpose }) => [],
54+
isMember: async (_user: string, _name: string, _org: string | null) => false,
55+
queuesOf: async () => [],
56+
};
57+
58+
/** Every call shape the card's acceptance names; the unknown values must NOT compile. */
59+
async function callShapes(svc: IOwnershipQueueService): Promise<unknown[]> {
60+
return [
61+
await svc.expandMembers('q', 'org_a'),
62+
await svc.expandMembers('q', null, {}),
63+
await svc.expandMembers('q', 'org_a', { purpose: 'addressing' }),
64+
await svc.expandMembers('q', 'org_a', { onFailedRead: 'refuse' }),
65+
await svc.expandMembers('q', 'org_a', { purpose: 'addressing', onFailedRead: 'refuse' }),
66+
await svc.expandMembers('q', 'org_a', { onFailedRead: 'nobody' }),
67+
await svc.isMember('u', 'q', 'org_a'),
68+
await svc.isMember('u', 'q', 'org_a', { onFailedRead: 'refuse' }),
69+
// @ts-expect-error — not a failed-read answer
70+
await svc.expandMembers('q', 'org_a', { onFailedRead: 'throw' }),
71+
// @ts-expect-error — not a failed-read answer
72+
await svc.isMember('u', 'q', 'org_a', { onFailedRead: 'empty' }),
73+
// @ts-expect-error — `purpose` is the other axis, and `isMember` does not take it
74+
await svc.isMember('u', 'q', 'org_a', { purpose: 'addressing' }),
75+
// @ts-expect-error — a purpose value is not a failed-read answer
76+
await svc.expandMembers('q', 'org_a', { onFailedRead: 'addressing' }),
77+
];
78+
}
79+
80+
describe('IOwnershipQueueService onFailedRead (#30251)', () => {
81+
it('every call shape compiles against an implementation written before the key, and answers as it did', async () => {
82+
expect(await callShapes(legacy)).toEqual([[], [], [], [], [], [], false, false, [], false, false, []]);
83+
});
84+
});

‎packages/spec/src/contracts/ownership-queue-service.ts‎

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
* answers `[]`. Each consumer says what it does with that (ADR-0142 D5, D6,
3636
* D7) — and every queue-dependent ADMISSION fails closed when the service is
3737
* not registered at all.
38+
*
39+
* A read that FAILS is not absence. What the expander answers then is the
40+
* caller's to say, per call — see {@link OwnershipQueueFailedReadAnswer}.
3841
*/
3942

4043
import type { QueueMemberType } from '../security/queue.zod.js';
@@ -45,6 +48,47 @@ export const OWNERSHIP_QUEUE_SERVICE = 'ownership-queue';
4548
/** Which reader an expansion is for — see {@link IOwnershipQueueService.expandMembers}. */
4649
export type QueueExpansionPurpose = 'access' | 'addressing';
4750

51+
/**
52+
* What a membership read answers when a read behind the queue FAILS — the
53+
* `onFailedRead` option of {@link IOwnershipQueueService.expandMembers} and
54+
* {@link IOwnershipQueueService.isMember} (ADR-0142, D2 "Membership", the
55+
* amended paragraph "An empty expansion is a fact; a failed read is a fault").
56+
*
57+
* The tables behind a queue are its own row (`sys_queue`, through which an
58+
* authored queue resolves), its member rows (`sys_queue_member`), and the
59+
* member tables a specification reads: `sys_team_member`, `sys_user_position`,
60+
* `sys_member` (through the position transition limb), `sys_business_unit_member`
61+
* and the business-unit tree (`sys_business_unit`). A table the composition
62+
* does not carry is absence, not failure. The activation-ledger read
63+
* (`sys_metadata_activation`, which says whether a declared queue or a position
64+
* is switched off) is NOT a failed read in this sense: under either answer its
65+
* failure reads "switched on", as it did before the key existed; whether that
66+
* posture moves is #29604's code half to decide.
67+
*
68+
* - `'nobody'` — the default and the answer every caller had before the key
69+
* existed. A failed read is logged at `warn`, and what sat behind it
70+
* contributes nobody. The admission consumers take it, because for them
71+
* nobody denies: the sitting-record access path (D3), the claim admission
72+
* (D4) and the assignment pool (D5).
73+
* - `'refuse'` — a failed read rejects the call with a failure that names the
74+
* read, never `[]`, a partial set or `false`. The recompute consumers take
75+
* it, because they turn the member set into routed work or materialised
76+
* grants and "nobody" there revokes: the approver slate (D6) and the
77+
* sharing-recipient grant recompute (D7). An implementation written before
78+
* this key existed ignores it and answers `'nobody'`; the registered one,
79+
* `@objectstack/plugin-sharing`'s, honours `'refuse'` from #29604's code half
80+
* on, so until then a caller passing `'refuse'` still gets `'nobody'`.
81+
*
82+
* Under either answer an EMPTY queue — every read succeeded and reached
83+
* nobody — is still `[]`, and a name that resolves nowhere is still `[]` /
84+
* `false`: those are facts, not faults.
85+
*
86+
* ⛔ An axis independent of {@link QueueExpansionPurpose}: which holders an
87+
* answer keeps and whether a failed read refuses are two questions, and one
88+
* value never answers both.
89+
*/
90+
export type OwnershipQueueFailedReadAnswer = 'nobody' | 'refuse';
91+
4892
/** One member specification: a recipient kind and the value that kind takes in a sharing rule. */
4993
export interface QueueMemberSpec {
5094
type: QueueMemberType;
@@ -92,15 +136,35 @@ export interface IOwnershipQueueService {
92136
* off — the sitting-record path, the claim, the assignment pool and the
93137
* sharing recipient all confer access. `'addressing'` keeps them — the
94138
* approver slate routes work, and dropping holders there is fail-open.
139+
*
140+
* `onFailedRead` says what the call is owed when a read behind the queue
141+
* fails: `'nobody'`, the default, or `'refuse'`, under which the call
142+
* rejects with a failure naming the read. An empty queue is still `[]`.
143+
* See {@link OwnershipQueueFailedReadAnswer} for the tables and for which
144+
* ADR-0142 consumer takes which answer; it is independent of `purpose`.
95145
*/
96146
expandMembers(
97147
name: string,
98148
organizationId: string | null,
99-
options?: { purpose?: QueueExpansionPurpose },
149+
options?: { purpose?: QueueExpansionPurpose; onFailedRead?: OwnershipQueueFailedReadAnswer },
100150
): Promise<string[]>;
101151

102-
/** Is `userId` a member of the queue named `name` in `organizationId`? `false` when the name resolves nowhere. */
103-
isMember(userId: string, name: string, organizationId: string | null): Promise<boolean>;
152+
/**
153+
* Is `userId` a member of the queue named `name` in `organizationId`?
154+
* `false` when the name resolves nowhere.
155+
*
156+
* `onFailedRead` is {@link expandMembers}' key with the same meaning:
157+
* under `'nobody'`, the default, a failed read counts as not reaching
158+
* `userId`; under `'refuse'`, `false` is answered only when every read
159+
* behind it succeeded, and a failed read rejects with a failure naming it.
160+
* The options argument is optional, so a three-argument call is today's.
161+
*/
162+
isMember(
163+
userId: string,
164+
name: string,
165+
organizationId: string | null,
166+
options?: { onFailedRead?: OwnershipQueueFailedReadAnswer },
167+
): Promise<boolean>;
104168

105169
/**
106170
* The machine names of every queue `userId` is a member of in

0 commit comments

Comments
 (0)