Initial Commit
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
# `useSharing` hook
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`useSharing` is the React-tier binding for `@suaveplan/drive-sharing`. It
|
||||
holds a local list of `ShareRecipient` rows that mirror the active grants
|
||||
on a partition, and exposes four async actions (`addRecipient`,
|
||||
`removeRecipient`, `updatePermission`, `refresh`) that delegate to the
|
||||
real engine — no in-memory stub, no mocks. After every successful action
|
||||
the hook re-syncs from `GrantManager.list({ partitionId, active: true })`
|
||||
so optimistic UI never drifts from the engine's truth.
|
||||
|
||||
## Features
|
||||
|
||||
- Wires directly to live `PartitionManager`, `GrantManager`, and
|
||||
`TokenManager` instances supplied by the caller. The package does not
|
||||
own a partition store.
|
||||
- AbortSignal threaded through every action. A pre-aborted signal raises
|
||||
a `DriveReactSharingError` before any engine mutation.
|
||||
- Surfaces failures via both `result.error` (for inline rendering) and
|
||||
`throw` (for promise-chain orchestration). Underlying engine errors
|
||||
are preserved on `cause`.
|
||||
- Emits three OTel counters: `drive.react.sharing.recipients_added_total`,
|
||||
`drive.react.sharing.recipients_removed_total`,
|
||||
`drive.react.sharing.permissions_updated_total{permission}`.
|
||||
- Empty-string recipient ids raise `INVALID_RECIPIENT` synchronously.
|
||||
- Permission update is a revoke-then-create round-trip because the engine
|
||||
has no atomic mutate API on `GrantManager`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```tsx
|
||||
import {
|
||||
GrantManager,
|
||||
InMemoryPartitionStore,
|
||||
KeyRing,
|
||||
PartitionManager,
|
||||
TokenManager,
|
||||
userId,
|
||||
} from "@suaveplan/drive-sharing";
|
||||
import { useSharing } from "@suaveplan/drive-react-sharing";
|
||||
|
||||
const partitions = new PartitionManager({
|
||||
store: new InMemoryPartitionStore(),
|
||||
keyRing: new KeyRing(),
|
||||
});
|
||||
const grants = new GrantManager();
|
||||
const tokens = new TokenManager();
|
||||
|
||||
function PanelWidget({ partitionId }: { partitionId: string }) {
|
||||
const sharing = useSharing({
|
||||
partitionId,
|
||||
grantorId: userId("alice"),
|
||||
partitions,
|
||||
grants,
|
||||
tokens,
|
||||
});
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => sharing.addRecipient("bob", "read")}
|
||||
>
|
||||
Add bob
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Composing with `AbortController` so a parent dialog can cancel inflight
|
||||
mutations on close:
|
||||
|
||||
```tsx
|
||||
import { useEffect } from "react";
|
||||
|
||||
function Composer({
|
||||
partitionId,
|
||||
sharing,
|
||||
}: {
|
||||
partitionId: string;
|
||||
sharing: ReturnType<typeof useSharing>;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
const ctrl = new AbortController();
|
||||
return () => ctrl.abort();
|
||||
}, [partitionId]);
|
||||
|
||||
async function bulkPromote(ids: readonly string[]): Promise<void> {
|
||||
const ctrl = new AbortController();
|
||||
for (const id of ids) {
|
||||
await sharing.updatePermission(id, "admin", ctrl.signal);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `useSharing(options: UseSharingOptions): UseSharingResult`
|
||||
- `options.partitionId: string` — partition the grants belong to.
|
||||
- `options.grantorId: UserId` — typically the current user.
|
||||
- `options.partitions: PartitionManager` — engine partition manager.
|
||||
- `options.grants: GrantManager` — engine grant manager.
|
||||
- `options.tokens: TokenManager` — engine token manager.
|
||||
- `options.initialRecipients?: readonly ShareRecipient[]` — optional
|
||||
SSR / Storybook seed; when omitted the hook seeds from
|
||||
`grants.list({ partitionId, active: true })`.
|
||||
- Returns `UseSharingResult` — `{ recipients, loading, error,
|
||||
addRecipient(id, perm, signal?), removeRecipient(grantId, signal?),
|
||||
updatePermission(grantId, perm, signal?), refresh(signal?) }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The recipient list is refreshed by re-reading from the engine after
|
||||
every mutation rather than spliced in place. This keeps the hook free
|
||||
of merge-conflict logic when multiple components share the same
|
||||
`GrantManager`. `primaryPermission` on each row is computed at projection
|
||||
time from a fixed tier ranking (`read < write < delete < share <
|
||||
admin`); consumers should still consult the full `permissions` array
|
||||
when making entitlement decisions, but UI badges can render the primary
|
||||
tier directly.
|
||||
Reference in New Issue
Block a user