Skip to main content

@form-guardian/core

Headless core for draft management. Framework-agnostic storage operations.

Installation

npm install @form-guardian/core

saveDraftCore(draftId, values, options?)

Save draft to storage.

Import:

import { saveDraftCore } from '@form-guardian/core';

Parameters:

  • draftId (string) - Unique draft identifier
  • values (T) - Values to save
  • options (object, optional) - Storage options
    • formId (string, optional) - Form identifier
    • origin (string, optional) - Origin URL
    • ttl (number | object, optional) - Time to live

Returns: Promise<void>

Example:

await saveDraftCore('form-id', 
{ name: 'John', email: 'john@example.com' },
{
formId: 'contact-form',
origin: 'https://example.com',
ttl: { days: 7 },
}
);

loadDraftCore(draftId, options?)

Load draft from storage.

Import:

import { loadDraftCore } from '@form-guardian/core';

Parameters:

  • draftId (string) - Unique draft identifier
  • options (object, optional) - Storage options

Returns: Promise<DraftData<T> | null>

Example:

const draft = await loadDraftCore('form-id');
if (draft) {
console.log(draft.values); // { name: 'John', email: 'john@example.com' }
console.log(draft.updatedAt); // 1699564800000
console.log(draft.formId); // 'contact-form'
console.log(draft.origin); // 'https://example.com'
}

clearDraft(draftId, options?)

Clear draft from storage.

Import:

import { clearDraft } from '@form-guardian/core';

Parameters:

  • draftId (string) - Unique draft identifier
  • options (object, optional) - Storage options

Returns: Promise<void>

Example:

await clearDraft('form-id');

hasDraftCore(draftId, options?)

Check if draft exists.

Import:

import { hasDraftCore } from '@form-guardian/core';

Parameters:

  • draftId (string) - Unique draft identifier
  • options (object, optional) - Storage options

Returns: Promise<boolean>

Example:

const exists = await hasDraftCore('form-id');
if (exists) {
console.log('Draft found!');
}

makeDraftId(formId, options?)

Generate draft ID from form ID and options.

Import:

import { makeDraftId } from '@form-guardian/core';

Parameters:

  • formId (string) - Form identifier
  • options (object, optional) - Configuration
    • includeOrigin (boolean) - Include origin in ID (default: true)
    • prefix (string) - Storage key prefix (default: 'fg')

Returns: string

Example:

const draftId = makeDraftId('my-form', {
includeOrigin: true,
prefix: 'fg',
});
// Returns: 'fg:https://example.com:my-form'

// Without origin
const simpleId = makeDraftId('my-form', {
includeOrigin: false,
prefix: 'app',
});
// Returns: 'app:my-form'

Types

DraftData<T>

Draft data structure returned by loadDraftCore.

interface DraftData<T> {
values: T; // Draft values
updatedAt: number; // Timestamp
formId?: string; // Form identifier
origin?: string; // Origin URL
ttl?: number | { // Time to live
days?: number;
hours?: number;
minutes?: number;
};
}

DraftMeta

Draft metadata without values.

interface DraftMeta {
updatedAt: number;
formId?: string;
origin?: string;
ttl?: number | { days?: number; hours?: number; minutes?: number };
}

Use Cases

Custom Storage Backend

import { saveDraftCore, loadDraftCore, clearDraft } from '@form-guardian/core';

class CustomDraftManager {
async save(formId: string, values: any) {
const draftId = `custom:${formId}`;
await saveDraftCore(draftId, values, {
formId,
ttl: { hours: 24 },
});
}

async load(formId: string) {
const draftId = `custom:${formId}`;
const draft = await loadDraftCore(draftId);
return draft?.values || null;
}

async clear(formId: string) {
const draftId = `custom:${formId}`;
await clearDraft(draftId);
}
}

const manager = new CustomDraftManager();
await manager.save('contact-form', { name: 'John' });
const values = await manager.load('contact-form');
await manager.clear('contact-form');

Server-Side Rendering (SSR)

import { hasDraftCore, loadDraftCore } from '@form-guardian/core';

// Check draft status on server
export async function getServerSideProps({ req }) {
const formId = 'user-profile';
const draftId = `fg:${req.headers.origin}:${formId}`;

const hasDraft = await hasDraftCore(draftId);

return {
props: {
hasDraft,
formId,
},
};
}

// Client-side: load draft if exists
function ProfileForm({ hasDraft, formId }) {
useEffect(() => {
if (hasDraft) {
loadDraftCore(formId).then(draft => {
if (draft) {
// Populate form with draft.values
}
});
}
}, [hasDraft, formId]);

// ...
}

Mobile App (React Native)

import { saveDraftCore, loadDraftCore } from '@form-guardian/core';
import AsyncStorage from '@react-native-async-storage/async-storage';

// Custom storage adapter for React Native
class MobileStorage {
async saveDraft(formId: string, values: any) {
const key = `draft:${formId}`;
const data = {
values,
updatedAt: Date.now(),
formId,
};
await AsyncStorage.setItem(key, JSON.stringify(data));
}

async loadDraft(formId: string) {
const key = `draft:${formId}`;
const json = await AsyncStorage.getItem(key);
return json ? JSON.parse(json) : null;
}

async clearDraft(formId: string) {
const key = `draft:${formId}`;
await AsyncStorage.removeItem(key);
}
}

Draft Synchronization

import { saveDraftCore, loadDraftCore } from '@form-guardian/core';

class DraftSync {
async syncToServer(formId: string) {
const draft = await loadDraftCore(formId);
if (!draft) return;

// Send to server
await fetch('/api/drafts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
formId: draft.formId,
values: draft.values,
updatedAt: draft.updatedAt,
}),
});
}

async syncFromServer(formId: string) {
const response = await fetch(`/api/drafts/${formId}`);
const serverDraft = await response.json();

if (serverDraft) {
await saveDraftCore(formId, serverDraft.values, {
formId: serverDraft.formId,
});
}
}
}

TTL-based Cleanup

import { loadDraftCore, clearDraft, makeDraftId } from '@form-guardian/core';

async function cleanupExpiredDrafts(formIds: string[]) {
const now = Date.now();

for (const formId of formIds) {
const draftId = makeDraftId(formId);
const draft = await loadDraftCore(draftId);

if (!draft) continue;

// Check if draft has expired
const ttlMs = parseTTL(draft.ttl);
const expiresAt = draft.updatedAt + ttlMs;

if (now > expiresAt) {
console.log(`Clearing expired draft: ${formId}`);
await clearDraft(draftId);
}
}
}

function parseTTL(ttl: any): number {
if (typeof ttl === 'number') return ttl;
if (!ttl) return Infinity;

const ms =
(ttl.days || 0) * 24 * 60 * 60 * 1000 +
(ttl.hours || 0) * 60 * 60 * 1000 +
(ttl.minutes || 0) * 60 * 1000;

return ms || Infinity;
}