UNPKG

fluid-pwa

Version:

🚀 The Ultimate Offline-First Progressive Web App Framework - Rapid PWA development with multiple batteries for seamless offline experiences

587 lines (583 loc) • 20.4 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropNames = Object.getOwnPropertyNames; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/lib/dexie/index.ts var index_exports = {}; __export(index_exports, { FluidPWADatabase: () => FluidPWADatabase, FluidPWAProvider: () => FluidPWAProvider, FluidPWAStatus: () => FluidPWAStatus, getFluidPWADatabase: () => getFluidPWADatabase, initializeFluidPWA: () => initializeFluidPWA, isFluidPWAInitialized: () => isFluidPWAInitialized, useAddItem: () => useAddItem, useBulkOperations: () => useBulkOperations, useDeleteItem: () => useDeleteItem, useFluidPWA: () => useFluidPWA, useFluidPWADatabase: () => useFluidPWADatabase, useFluidPWAStats: () => useFluidPWAStats, useGetAllItems: () => useGetAllItems, useGetAllPendingItems: () => useGetAllPendingItems, useGetItem: () => useGetItem, useGetItemsBySyncStatus: () => useGetItemsBySyncStatus, useGetPendingItems: () => useGetPendingItems, useUpdateItem: () => useUpdateItem, withFluidPWA: () => withFluidPWA }); module.exports = __toCommonJS(index_exports); // src/lib/dexie/database.ts var import_dexie = __toESM(require("dexie")); var import_uuid = require("uuid"); var FluidPWADatabase = class extends import_dexie.default { constructor(config) { super(config.databaseName); this.isInitialized = false; this.config = config; this.setupDatabase(); } setupDatabase() { const version = this.config.version || 1; this.version(version).stores(this.config.schema); this.setupHooks(); this.isInitialized = true; if (this.config.enableLogging) { console.log(`Fluid-PWA: Database "${this.config.databaseName}" initialized with schema:`, this.config.schema); } } setupHooks() { Object.keys(this.config.schema).forEach((storeName) => { const table = this[storeName]; if (table) { table.hook("creating", (primKey, obj, trans) => { this.populateOfflineFields(obj, "NEW"); }); table.hook("updating", (modifications, primKey, obj, trans) => { if (!modifications.lastModifiedOffline) { modifications.lastModifiedOffline = Date.now(); } if (!modifications.syncStatus && obj.syncStatus === "SYNCED") { modifications.syncStatus = "PENDING_UPDATE"; } }); } }); } /** * Populate required offline fields for new items */ populateOfflineFields(item, syncStatus) { if (!item.localId) { item.localId = (0, import_uuid.v4)(); } if (!item.syncStatus) { item.syncStatus = syncStatus; } if (!item.lastModifiedOffline) { item.lastModifiedOffline = Date.now(); } if (this.config.userId && !item.userId) { item.userId = this.config.userId; } } /** * Get all stores defined in the schema */ getStoreNames() { return Object.keys(this.config.schema); } /** * Get a table by name with type safety */ getTable(storeName) { const table = this[storeName]; if (!table) { throw new Error(`Store "${storeName}" not found in database schema`); } return table; } /** * Check if database is properly initialized */ isReady() { return this.isInitialized && this.isOpen(); } /** * Get configuration */ getConfig() { return __spreadValues({}, this.config); } /** * Create a new item with proper offline fields */ async createItem(storeName, payload, syncStatus = "PENDING_CREATE") { const table = this.getTable(storeName); const item = __spreadValues(__spreadProps(__spreadValues({}, payload), { localId: (0, import_uuid.v4)(), syncStatus, lastModifiedOffline: Date.now() }), this.config.userId && { userId: this.config.userId }); await table.add(item); return item.localId; } /** * Update an item and mark for sync */ async updateItem(storeName, localId, updates) { const table = this.getTable(storeName); const item = await table.get(localId); if (!item) { throw new Error(`Item with localId "${localId}" not found in store "${storeName}"`); } const updateData = __spreadProps(__spreadValues({}, updates), { lastModifiedOffline: Date.now(), syncStatus: item.syncStatus === "SYNCED" ? "PENDING_UPDATE" : item.syncStatus }); return table.update(localId, updateData); } /** * Delete an item (soft delete with sync status) */ async deleteItem(storeName, localId) { const table = this.getTable(storeName); const item = await table.get(localId); if (!item) { throw new Error(`Item with localId "${localId}" not found in store "${storeName}"`); } if (item.syncStatus === "NEW" || item.syncStatus === "PENDING_CREATE") { await table.delete(localId); } else { await table.update(localId, { syncStatus: "PENDING_DELETE", lastModifiedOffline: Date.now() }); } } /** * Get items by sync status */ async getItemsBySyncStatus(storeName, syncStatus) { const table = this.getTable(storeName); const statuses = Array.isArray(syncStatus) ? syncStatus : [syncStatus]; return table.where("syncStatus").anyOf(statuses).toArray(); } /** * Get all pending items across all stores (for sync queue) */ async getAllPendingItems() { const pendingStatuses = ["PENDING_CREATE", "PENDING_UPDATE", "PENDING_DELETE"]; const allPending = []; for (const storeName of this.getStoreNames()) { const items = await this.getItemsBySyncStatus(storeName, pendingStatuses); items.forEach((item) => { allPending.push(__spreadProps(__spreadValues({}, item), { storeName })); }); } return allPending; } }; var dbInstance = null; function initializeFluidPWA(config) { if (dbInstance) { console.warn("Fluid-PWA: Database already initialized. Returning existing instance."); return dbInstance; } dbInstance = new FluidPWADatabase(config); return dbInstance; } function getFluidPWADatabase() { if (!dbInstance) { throw new Error("Fluid-PWA: Database not initialized. Call initializeFluidPWA() first."); } return dbInstance; } function isFluidPWAInitialized() { return dbInstance !== null && dbInstance.isReady(); } // src/lib/dexie/hooks.ts var import_react = require("react"); var import_dexie_react_hooks = require("dexie-react-hooks"); function useAddItem(storeName, options = {}) { const db = getFluidPWADatabase(); return (0, import_react.useCallback)(async (payload, syncStatus = "PENDING_CREATE") => { try { let processedPayload = payload; if (options.onBeforeAdd) { processedPayload = await options.onBeforeAdd(payload); } const localId = await db.createItem(storeName, processedPayload, syncStatus); if (options.onAfterAdd) { await options.onAfterAdd(processedPayload, localId); } return { success: true, data: localId }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Unknown error occurred" }; } }, [db, storeName, options]); } function useGetItem(storeName, localId) { return (0, import_dexie_react_hooks.useLiveQuery)( async () => { if (!localId) return void 0; const db = getFluidPWADatabase(); const table = db.getTable(storeName); return table.get(localId); }, [storeName, localId] ); } function useGetAllItems(storeName, queryOptions = {}) { return (0, import_dexie_react_hooks.useLiveQuery)( async () => { const db = getFluidPWADatabase(); const table = db.getTable(storeName); let query = table.toCollection(); if (queryOptions.orderBy) { query = table.orderBy(queryOptions.orderBy); if (queryOptions.reverse) { query = query.reverse(); } } if (queryOptions.offset) { query = query.offset(queryOptions.offset); } if (queryOptions.limit) { query = query.limit(queryOptions.limit); } let results = await query.toArray(); if (queryOptions.filter) { results = results.filter(queryOptions.filter); } return results; }, [storeName, queryOptions.orderBy, queryOptions.reverse, queryOptions.offset, queryOptions.limit] ); } function useUpdateItem(storeName, options = {}) { const db = getFluidPWADatabase(); return (0, import_react.useCallback)(async (localId, updates) => { try { let processedUpdates = updates; if (options.onBeforeUpdate) { const existingItem = await db.getTable(storeName).get(localId); processedUpdates = await options.onBeforeUpdate(existingItem, updates); } const result = await db.updateItem(storeName, localId, processedUpdates); if (options.onAfterUpdate) { const updatedItem = await db.getTable(storeName).get(localId); await options.onAfterUpdate(updatedItem, result); } return { success: true, data: result }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Unknown error occurred" }; } }, [db, storeName, options]); } function useDeleteItem(storeName, options = {}) { const db = getFluidPWADatabase(); return (0, import_react.useCallback)(async (localId) => { try { if (options.onBeforeDelete) { const shouldDelete = await options.onBeforeDelete(localId); if (!shouldDelete) { return { success: false, error: "Delete operation cancelled by before hook" }; } } await db.deleteItem(storeName, localId); if (options.onAfterDelete) { await options.onAfterDelete(localId); } return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Unknown error occurred" }; } }, [db, storeName, options]); } function useGetItemsBySyncStatus(storeName, syncStatus) { return (0, import_dexie_react_hooks.useLiveQuery)( async () => { const db = getFluidPWADatabase(); return db.getItemsBySyncStatus(storeName, syncStatus); }, [storeName, syncStatus] ); } function useGetPendingItems(storeName) { const pendingStatuses = ["PENDING_CREATE", "PENDING_UPDATE", "PENDING_DELETE"]; return useGetItemsBySyncStatus(storeName, pendingStatuses); } function useGetAllPendingItems() { return (0, import_dexie_react_hooks.useLiveQuery)( async () => { const db = getFluidPWADatabase(); return db.getAllPendingItems(); }, [] ); } function useFluidPWAStats() { return (0, import_dexie_react_hooks.useLiveQuery)( async () => { const db = getFluidPWADatabase(); const stats = {}; for (const storeName of db.getStoreNames()) { const table = db.getTable(storeName); const total = await table.count(); const pending = await db.getItemsBySyncStatus(storeName, ["PENDING_CREATE", "PENDING_UPDATE", "PENDING_DELETE"]); const synced = await db.getItemsBySyncStatus(storeName, "SYNCED"); const errors = await db.getItemsBySyncStatus(storeName, "ERROR"); stats[storeName] = { total, pending: pending.length, synced: synced.length, errors: errors.length }; } return stats; }, [] ); } function useBulkOperations(storeName) { const db = getFluidPWADatabase(); const bulkAdd = (0, import_react.useCallback)(async (items) => { try { const table = db.getTable(storeName); const processedItems = items.map((item) => __spreadValues(__spreadProps(__spreadValues({}, item), { localId: item.localId || crypto.randomUUID(), syncStatus: "PENDING_CREATE", lastModifiedOffline: Date.now() }), db.getConfig().userId && { userId: db.getConfig().userId })); await table.bulkAdd(processedItems); return { success: true, data: processedItems.map((item) => item.localId) }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Bulk add failed" }; } }, [db, storeName]); const bulkUpdate = (0, import_react.useCallback)(async (updates) => { try { const table = db.getTable(storeName); let totalUpdated = 0; for (const { localId, changes } of updates) { const updated = await db.updateItem(storeName, localId, changes); totalUpdated += updated; } return { success: true, data: totalUpdated }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Bulk update failed" }; } }, [db, storeName]); const bulkDelete = (0, import_react.useCallback)(async (localIds) => { try { let totalDeleted = 0; for (const localId of localIds) { await db.deleteItem(storeName, localId); totalDeleted++; } return { success: true, data: totalDeleted }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Bulk delete failed" }; } }, [db, storeName]); return { bulkAdd, bulkUpdate, bulkDelete }; } // src/lib/dexie/FluidPWAProvider.tsx var import_react2 = __toESM(require("react")); var FluidPWAContext = (0, import_react2.createContext)({ database: null, isInitialized: false, isLoading: true, error: null, config: null }); function FluidPWAProvider({ children, config, onInitialized, onError }) { const [database, setDatabase] = (0, import_react2.useState)(null); const [isLoading, setIsLoading] = (0, import_react2.useState)(true); const [error, setError] = (0, import_react2.useState)(null); (0, import_react2.useEffect)(() => { const initDatabase = async () => { try { setIsLoading(true); setError(null); let db; if (isFluidPWAInitialized()) { db = getFluidPWADatabase(); if (config.enableLogging) { console.log("Fluid-PWA: Using existing database instance"); } } else { if (config.enableLogging) { console.log("Fluid-PWA: Initializing database with config:", config); } db = initializeFluidPWA(config); } await db.open(); setDatabase(db); onInitialized == null ? void 0 : onInitialized(db); if (config.enableLogging) { console.log("Fluid-PWA: Database successfully initialized and ready"); } } catch (err) { const errorMessage = err instanceof Error ? err.message : "Failed to initialize Fluid-PWA database"; setError(errorMessage); onError == null ? void 0 : onError(err instanceof Error ? err : new Error(errorMessage)); if (config.enableLogging) { console.error("Fluid-PWA: Database initialization failed:", err); } } finally { setIsLoading(false); } }; initDatabase(); }, [config, onInitialized, onError]); const contextValue = { database, isInitialized: database !== null && database.isReady(), isLoading, error, config }; return /* @__PURE__ */ import_react2.default.createElement(FluidPWAContext.Provider, { value: contextValue }, children); } function useFluidPWA() { const context = (0, import_react2.useContext)(FluidPWAContext); if (!context) { throw new Error("useFluidPWA must be used within a FluidPWAProvider"); } return context; } function useFluidPWADatabase() { const { database, isInitialized, error } = useFluidPWA(); if (error) { throw new Error(`Fluid-PWA Error: ${error}`); } if (!isInitialized || !database) { throw new Error("Fluid-PWA: Database not yet initialized"); } return database; } function withFluidPWA(Component, config) { return function WrappedComponent(props) { return /* @__PURE__ */ import_react2.default.createElement(FluidPWAProvider, { config }, /* @__PURE__ */ import_react2.default.createElement(Component, __spreadValues({}, props))); }; } function FluidPWAStatus({ children, loadingComponent, errorComponent }) { const { isInitialized, isLoading, error } = useFluidPWA(); if (error && errorComponent) { return /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, errorComponent(error)); } if (error) { return /* @__PURE__ */ import_react2.default.createElement("div", { className: "p-4 bg-red-50 border border-red-200 rounded-md" }, /* @__PURE__ */ import_react2.default.createElement("h3", { className: "text-red-800 font-medium" }, "Database Error"), /* @__PURE__ */ import_react2.default.createElement("p", { className: "text-red-600 text-sm mt-1" }, error)); } if (isLoading && loadingComponent) { return /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, loadingComponent); } if (isLoading) { return /* @__PURE__ */ import_react2.default.createElement("div", { className: "flex items-center justify-center p-8" }, /* @__PURE__ */ import_react2.default.createElement("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" }), /* @__PURE__ */ import_react2.default.createElement("span", { className: "ml-3 text-gray-600" }, "Initializing offline database...")); } if (!isInitialized) { return /* @__PURE__ */ import_react2.default.createElement("div", { className: "p-4 bg-yellow-50 border border-yellow-200 rounded-md" }, /* @__PURE__ */ import_react2.default.createElement("p", { className: "text-yellow-800" }, "Database not yet ready...")); } return /* @__PURE__ */ import_react2.default.createElement(import_react2.default.Fragment, null, children); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { FluidPWADatabase, FluidPWAProvider, FluidPWAStatus, getFluidPWADatabase, initializeFluidPWA, isFluidPWAInitialized, useAddItem, useBulkOperations, useDeleteItem, useFluidPWA, useFluidPWADatabase, useFluidPWAStats, useGetAllItems, useGetAllPendingItems, useGetItem, useGetItemsBySyncStatus, useGetPendingItems, useUpdateItem, withFluidPWA }); //# sourceMappingURL=index.js.map