Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions deps/scrapbox-rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ export type {
NotPrivilegeError,
Page,
PageList,
PageSnapshot,
ProjectId,
ProjectResponse,
ProjectSearchResult,
SearchedTitle,
SearchResult,
SessionError,
Snapshot,
TweetInfo,
} from "https://raw.githubusercontent.com/scrapbox-jp/types/0.3.5/rest.ts";
150 changes: 150 additions & 0 deletions rest/getSnapshots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import type {
ErrorLike,
NotFoundError,
NotLoggedInError,
NotMemberError,
PageSnapshot,
Snapshot,
} from "../deps/scrapbox-rest.ts";
import { tryToErrorLike } from "../is.ts";
import { cookie } from "./auth.ts";
import { BaseOptions, Result, setDefaults } from "./util.ts";
import { UnexpectedResponseError } from "./error.ts";

/** 不正なfollowingIdを渡されたときに発生するエラー */
export interface InvalidPageSnapshotIdError extends ErrorLike {
name: "InvalidPageSnapshotIdError";
}

export interface GetSnapshotsOptions extends BaseOptions {
/** 次のsnapshots listを示すID */
followingId?: string;
}

/** get page snapshots
*
* @param options connect.sid etc.
*/
export const getSnapshots = async (
project: string,
pageId: string,
options?: GetSnapshotsOptions,
): Promise<
Result<
(PageSnapshot & { followingId: string }),
| NotFoundError
| NotLoggedInError
| NotMemberError
| InvalidPageSnapshotIdError
>
> => {
const { sid, hostName, fetch, followingId } = setDefaults(options ?? {});
const path = `https://${hostName}/api/page-snapshots/${project}/${pageId}/${
followingId ? `?followingId=${followingId}` : ""
}`;

const res = await fetch(
path,
sid ? { headers: { Cookie: cookie(sid) } } : undefined,
);

if (!res.ok) {
if (res.status === 422) {
return {
ok: false,
value: {
name: "InvalidPageSnapshotIdError",
message: await res.text(),
},
};
}
const text = await res.text();
const value = tryToErrorLike(text);
if (!value) {
throw new UnexpectedResponseError({
path: new URL(path),
...res,
body: text,
});
}
return {
ok: false,
value: value as (NotFoundError | NotLoggedInError | NotMemberError),
};
}

const data = (await res.json()) as PageSnapshot;
return {
ok: true,
value: { ...data, followingId: res.headers.get("X-following-id") ?? "" },
};
};

/** 指定したページのsnapshotsを、responseに入っている塊ごとに全て返す
*
* @param project ページのproject name
* @param pageId page id
* @return 認証が通らなかったらエラーを、通ったらasync generatorを返す
*/
export const readSnapshotsBulk = async (
project: string,
pageId: string,
options?: BaseOptions,
): Promise<
| NotFoundError
| NotLoggedInError
| NotMemberError
| InvalidPageSnapshotIdError
| AsyncGenerator<Snapshot[], void, unknown>
> => {
const first = await getSnapshots(project, pageId, options);
if (!first.ok) return first.value;

return async function* () {
yield first.value.snapshots;
let followingId = first.value.followingId;

while (followingId) {
const result = await getSnapshots(project, pageId, {
followingId,
...options,
});

// すでに認証は通っているので、ここでエラーになるはずがない
if (!result.ok) {
throw new Error("The authorization cannot be unavailable");
}
yield result.value.snapshots;
followingId = result.value.followingId;
}
}();
};

/** 指定したページの全てのsnapshotsを取得し、一つづつ返す
*
* @param project ページのproject name
* @param pageId page id
* @return 認証が通らなかったらエラーを、通ったらasync generatorを返す
*/
export const readSnapshots = async (
project: string,
pageId: string,
options?: BaseOptions,
): Promise<
| NotFoundError
| NotLoggedInError
| NotMemberError
| InvalidPageSnapshotIdError
| AsyncGenerator<Snapshot, void, unknown>
> => {
const reader = await readSnapshotsBulk(project, pageId, options);
if ("name" in reader) return reader;

return async function* () {
for await (const titles of reader) {
for (const title of titles) {
yield title;
}
}
}();
};