Files
suaveplan/packages/networking/drive/drive-react-sharing/src/hooks/use-sharing/use-sharing.ts
2026-05-27 23:47:53 +01:00

295 lines
9.8 KiB
TypeScript

import {
type Grant,
type GrantId,
type PartitionId,
type SharePermission,
grantId as toGrantId,
partitionId as toPartitionId,
userId as toUserId,
type UserId,
} from "@suaveplan/drive-sharing";
import { useCallback, useEffect, useRef, useState } from "react";
import {
DRIVE_REACT_SHARING_CODES,
DriveReactSharingError,
} from "../../error-codes.js";
import { getDriveReactSharingInstruments } from "../../otel/instruments.js";
import type {
ShareRecipient,
UseSharingOptions,
UseSharingResult,
} from "../../types/sharing.types.js";
/** Permission tier ordering for `primaryPermission` resolution. */
const TIER_RANK: Record<SharePermission, number> = {
read: 1,
write: 2,
delete: 3,
share: 4,
admin: 5,
};
function primaryOf(permissions: readonly SharePermission[]): SharePermission {
let best: SharePermission = "read";
let bestRank = 0;
for (const p of permissions) {
const rank = TIER_RANK[p];
if (rank > bestRank) {
best = p;
bestRank = rank;
}
}
return best;
}
function projectGrant(grant: Grant): ShareRecipient {
return {
grantId: grant.id,
recipientId: grant.granteeId,
permissions: grant.permissions,
primaryPermission: primaryOf(grant.permissions),
createdAt: grant.createdAt,
...(grant.expiresAt !== undefined
? { expiresAt: grant.expiresAt }
: {}),
revoked: grant.revoked,
};
}
function loadRecipients(options: UseSharingOptions): readonly ShareRecipient[] {
const pid = toPartitionId(options.partitionId) as PartitionId;
return options.grants
.list({ partitionId: pid, active: true })
.map(projectGrant);
}
function wrap(
code:
| typeof DRIVE_REACT_SHARING_CODES.ADD_FAILED
| typeof DRIVE_REACT_SHARING_CODES.REVOKE_FAILED
| typeof DRIVE_REACT_SHARING_CODES.UPDATE_FAILED
| typeof DRIVE_REACT_SHARING_CODES.REFRESH_FAILED,
messageKey: string,
cause: unknown
): DriveReactSharingError {
return new DriveReactSharingError(code, messageKey, { cause });
}
function checkAborted(signal: AbortSignal | undefined): void {
if (signal?.aborted === true) {
throw new DriveReactSharingError(
DRIVE_REACT_SHARING_CODES.ADD_FAILED,
"drive-react-sharing.aborted"
);
}
}
/**
* React hook binding a partition's share state to the live engine.
*
* Holds an in-memory `ShareRecipient[]` that mirrors active grants for the
* supplied `partitionId`, plus async action callbacks (`addRecipient`,
* `removeRecipient`, `updatePermission`, `refresh`) that delegate to
* `@suaveplan/drive-sharing` and re-sync state on success. Every action
* accepts an optional `AbortSignal`. Errors are surfaced both via
* `result.error` and by re-throwing so callers can render inline.
*/
export function useSharing(options: UseSharingOptions): UseSharingResult {
const optionsRef = useRef(options);
optionsRef.current = options;
const [recipients, setRecipients] = useState<readonly ShareRecipient[]>(
() => options.initialRecipients ?? loadRecipients(options)
);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<DriveReactSharingError | null>(null);
// Track mount status so async tasks do not setState after unmount.
const mountedRef = useRef(true);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
const safeSetRecipients = useCallback(
(next: readonly ShareRecipient[]): void => {
if (mountedRef.current) setRecipients(next);
},
[]
);
const safeSetLoading = useCallback((next: boolean): void => {
if (mountedRef.current) setLoading(next);
}, []);
const safeSetError = useCallback(
(next: DriveReactSharingError | null): void => {
if (mountedRef.current) setError(next);
},
[]
);
const refresh = useCallback(
async (signal?: AbortSignal): Promise<void> => {
safeSetLoading(true);
try {
checkAborted(signal);
const next = loadRecipients(optionsRef.current);
safeSetRecipients(next);
safeSetError(null);
} catch (err) {
const wrapped = wrap(
DRIVE_REACT_SHARING_CODES.REFRESH_FAILED,
"drive-react-sharing.refresh-failed",
err
);
safeSetError(wrapped);
throw wrapped;
} finally {
safeSetLoading(false);
}
},
[safeSetError, safeSetLoading, safeSetRecipients]
);
const addRecipient = useCallback(
async (
recipientId: string,
permission: SharePermission,
signal?: AbortSignal
): Promise<void> => {
if (recipientId.trim() === "") {
const e = new DriveReactSharingError(
DRIVE_REACT_SHARING_CODES.INVALID_RECIPIENT,
"drive-react-sharing.invalid-recipient"
);
safeSetError(e);
throw e;
}
safeSetLoading(true);
try {
checkAborted(signal);
const opts = optionsRef.current;
opts.grants.create({
grantorId: opts.grantorId,
granteeId: toUserId(recipientId) as UserId,
partitionId: toPartitionId(opts.partitionId) as PartitionId,
permissions: [permission],
});
getDriveReactSharingInstruments().recipientsAddedTotal.add(1, {
permission,
});
safeSetRecipients(loadRecipients(opts));
safeSetError(null);
} catch (err) {
const wrapped = wrap(
DRIVE_REACT_SHARING_CODES.ADD_FAILED,
"drive-react-sharing.add-failed",
err
);
safeSetError(wrapped);
throw wrapped;
} finally {
safeSetLoading(false);
}
},
[safeSetError, safeSetLoading, safeSetRecipients]
);
const removeRecipient = useCallback(
async (grantId: string, signal?: AbortSignal): Promise<void> => {
const opts = optionsRef.current;
const existing = opts.grants.get(toGrantId(grantId) as GrantId);
if (existing === undefined) {
const e = new DriveReactSharingError(
DRIVE_REACT_SHARING_CODES.GRANT_NOT_FOUND,
"drive-react-sharing.grant-not-found"
);
safeSetError(e);
throw e;
}
safeSetLoading(true);
try {
checkAborted(signal);
opts.grants.revoke(toGrantId(grantId) as GrantId);
getDriveReactSharingInstruments().recipientsRemovedTotal.add(1);
safeSetRecipients(loadRecipients(opts));
safeSetError(null);
} catch (err) {
const wrapped = wrap(
DRIVE_REACT_SHARING_CODES.REVOKE_FAILED,
"drive-react-sharing.revoke-failed",
err
);
safeSetError(wrapped);
throw wrapped;
} finally {
safeSetLoading(false);
}
},
[safeSetError, safeSetLoading, safeSetRecipients]
);
const updatePermission = useCallback(
async (
grantId: string,
permission: SharePermission,
signal?: AbortSignal
): Promise<void> => {
const opts = optionsRef.current;
const existing = opts.grants.get(toGrantId(grantId) as GrantId);
if (existing === undefined) {
const e = new DriveReactSharingError(
DRIVE_REACT_SHARING_CODES.GRANT_NOT_FOUND,
"drive-react-sharing.grant-not-found"
);
safeSetError(e);
throw e;
}
safeSetLoading(true);
try {
checkAborted(signal);
// Revoke + recreate is the engine-supported way to swap
// permissions atomically; GrantManager has no mutate API.
opts.grants.revoke(existing.id);
opts.grants.create({
grantorId: existing.grantorId,
granteeId: existing.granteeId,
partitionId: existing.partitionId,
permissions: [permission],
...(existing.expiresAt !== undefined
? { expiresAt: existing.expiresAt }
: {}),
});
getDriveReactSharingInstruments().permissionsUpdatedTotal.add(
1,
{ permission }
);
safeSetRecipients(loadRecipients(opts));
safeSetError(null);
} catch (err) {
const wrapped = wrap(
DRIVE_REACT_SHARING_CODES.UPDATE_FAILED,
"drive-react-sharing.update-failed",
err
);
safeSetError(wrapped);
throw wrapped;
} finally {
safeSetLoading(false);
}
},
[safeSetError, safeSetLoading, safeSetRecipients]
);
return {
recipients,
loading,
error,
addRecipient,
removeRecipient,
updatePermission,
refresh,
};
}