Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-use realtime event data to refresh collections #28

Merged
merged 4 commits into from
Feb 1, 2025
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: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const config = tseslint.config({
],
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/array-type": [
"error",
{
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "astro-loader-pocketbase",
"version": "2.2.1",
"version": "2.3.0-next.1",
"description": "A content loader for Astro that uses the PocketBase API",
"license": "MIT",
"author": "Luis Wolf <[email protected]> (https://pawcode.de)",
Expand Down
12 changes: 6 additions & 6 deletions src/cleanup-entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ export async function cleanupEntries(
// If the collection is locked, an superuser token is required
if (collectionRequest.status === 403) {
context.logger.error(
`(${options.collectionName}) The collection is not accessible without superuser rights. Please provide superuser credentials in the config.`
`The collection is not accessible without superuser rights. Please provide superuser credentials in the config.`
);
return;
}

const reason = await collectionRequest
.json()
.then((data) => data.message);
const errorMessage = `(${options.collectionName}) Fetching ids failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
const errorMessage = `Fetching ids failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
context.logger.error(errorMessage);
return;
}
Expand All @@ -74,7 +74,9 @@ export async function cleanupEntries(
let cleanedUp = 0;

// Get all ids of the entries in the store
const storedIds = context.store.values().map((entry) => entry.data.id) as Array<string>;
const storedIds = context.store
.values()
.map((entry) => entry.data.id) as Array<string>;
for (const id of storedIds) {
// If the id is not in the entries set, remove the entry from the store
if (!entries.has(id)) {
Expand All @@ -85,8 +87,6 @@ export async function cleanupEntries(

if (cleanedUp > 0) {
// Log the number of cleaned up entries
context.logger.info(
`(${options.collectionName}) Cleaned up ${cleanedUp} old entries.`
);
context.logger.info(`Cleaned up ${cleanedUp} old entries.`);
}
}
48 changes: 48 additions & 0 deletions src/handle-realtime-updates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { LoaderContext } from "astro/loaders";
import type { PocketBaseLoaderOptions } from "./types/pocketbase-loader-options.type";
import { isRealtimeData } from "./utils/is-realtime-data";
import { parseEntry } from "./utils/parse-entry";

/**
* Handles realtime updates for the loader without making any new network requests.
*
* Returns `true` if the data was handled and no further action is needed.
*/
export async function handleRealtimeUpdates(
context: LoaderContext,
options: PocketBaseLoaderOptions
): Promise<boolean> {
// Check if data was provided via the refresh context
if (!context.refreshContextData?.data) {
return false;
}

// Check if the data is PocketBase realtime data
const data = context.refreshContextData.data;
if (!isRealtimeData(data)) {
return false;
}

// Check if the collection name matches the current collection
if (data.record.collectionName !== options.collectionName) {
return false;
}

// Handle deleted entry
if (data.action === "delete") {
context.logger.info("Removing deleted entry");
context.store.delete(data.record.id);
return true;
}

// Handle updated or new entry
if (data.action === "update") {
context.logger.info("Updating outdated entry");
} else {
context.logger.info("Creating new entry");
}

// Parse the entry and store
await parseEntry(data.record, context, options);
return true;
}
16 changes: 6 additions & 10 deletions src/load-entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,9 @@ export async function loadEntries(

// Log the fetching of the entries
context.logger.info(
`(${options.collectionName}) Fetching${
lastModified ? " modified" : ""
} data${lastModified ? ` starting at ${lastModified}` : ""}${
superuserToken ? " as superuser" : ""
}`
`Fetching${lastModified ? " modified" : ""} data${
lastModified ? ` starting at ${lastModified}` : ""
}${superuserToken ? " as superuser" : ""}`
);

// Prepare pagination variables
Expand Down Expand Up @@ -64,15 +62,15 @@ export async function loadEntries(
// If the collection is locked, an superuser token is required
if (collectionRequest.status === 403) {
throw new Error(
`(${options.collectionName}) The collection is not accessible without superuser rights. Please provide superuser credentials in the config.`
`The collection is not accessible without superuser rights. Please provide superuser credentials in the config.`
);
}

// Get the reason for the error
const reason = await collectionRequest
.json()
.then((data) => data.message);
const errorMessage = `(${options.collectionName}) Fetching data failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
const errorMessage = `Fetching data failed with status code ${collectionRequest.status}.\nReason: ${reason}`;
throw new Error(errorMessage);
}

Expand All @@ -92,8 +90,6 @@ export async function loadEntries(

// Log the number of fetched entries
context.logger.info(
`(${options.collectionName}) Fetched ${entries}${
lastModified ? " changed" : ""
} entries.`
`Fetched ${entries}${lastModified ? " changed" : ""} entries.`
);
}
14 changes: 12 additions & 2 deletions src/pocketbase-loader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { Loader, LoaderContext } from "astro/loaders";
import type { ZodSchema } from "astro/zod";
import packageJson from "./../package.json";
import { cleanupEntries } from "./cleanup-entries";
import { generateSchema } from "./generate-schema";
import { handleRealtimeUpdates } from "./handle-realtime-updates";
import { loadEntries } from "./load-entries";
import type { PocketBaseLoaderOptions } from "./types/pocketbase-loader-options.type";
import { getSuperuserToken } from "./utils/get-superuser-token";
Expand All @@ -16,6 +18,8 @@ export function pocketbaseLoader(options: PocketBaseLoaderOptions): Loader {
return {
name: "pocketbase-loader",
load: async (context: LoaderContext): Promise<void> => {
context.logger.label = `pocketbase-loader:${options.collectionName}`;

// Check if the collection should be refreshed.
const refresh = shouldRefresh(
context.refreshContextData,
Expand All @@ -25,6 +29,12 @@ export function pocketbaseLoader(options: PocketBaseLoaderOptions): Loader {
return;
}

// Handle realtime updates
const handled = await handleRealtimeUpdates(context, options);
if (handled) {
return;
}

// Get the date of the last fetch to only update changed entries.
let lastModified = context.meta.get("last-modified");

Expand All @@ -45,7 +55,7 @@ export function pocketbaseLoader(options: PocketBaseLoaderOptions): Loader {
// Disable incremental builds if no updated field is provided
if (!options.updatedField) {
context.logger.info(
`(${options.collectionName}) No "updatedField" was provided. Incremental builds are disabled.`
`No "updatedField" was provided. Incremental builds are disabled.`
);
lastModified = undefined;
}
Expand Down Expand Up @@ -76,7 +86,7 @@ export function pocketbaseLoader(options: PocketBaseLoaderOptions): Loader {

context.meta.set("version", packageJson.version);
},
schema: async () => {
schema: async (): Promise<ZodSchema> => {
// Generate the schema for the collection according to the API
return await generateSchema(options);
}
Expand Down
34 changes: 34 additions & 0 deletions src/utils/is-realtime-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { z } from "astro/zod";

/**
* Schema for realtime data received from PocketBase.
*/
const realtimeDataSchema = z.object({
action: z.union([
z.literal("create"),
z.literal("update"),
z.literal("delete")
]),
record: z.object({
id: z.string(),
collectionName: z.string(),
collectionId: z.string()
})
});

/**
* Type for realtime data received from PocketBase.
*/
export type RealtimeData = z.infer<typeof realtimeDataSchema>;

/**
* Checks if the given data is realtime data received from PocketBase.
*/
export function isRealtimeData(data: unknown): data is RealtimeData {
try {
realtimeDataSchema.parse(data);
return true;
} catch {
return false;
}
}
2 changes: 1 addition & 1 deletion src/utils/parse-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function parseSchema(
function parseSingleOrMultipleValues(
field: PocketBaseSchemaEntry,
type: z.ZodType
) {
): z.ZodType {
// If the select allows multiple values, create an array of the enum
if (field.maxSelect === undefined || field.maxSelect === 1) {
return type;
Expand Down
Loading