@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 identifiervalues(T) - Values to saveoptions(object, optional) - Storage optionsformId(string, optional) - Form identifierorigin(string, optional) - Origin URLttl(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 identifieroptions(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 identifieroptions(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 identifieroptions(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 identifieroptions(object, optional) - ConfigurationincludeOrigin(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;
}