local-lite 0.0.14 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ import { AccessGuard } from './guard.js';
2
+ /**
3
+ * Wraps a LocalFirstClient with an AccessGuard.
4
+ * Intercepts CRUD queries and mutations, asserting permissions before writes and
5
+ * filtering unauthorized rows or restricted fields on reads.
6
+ */
7
+ export declare function withAccess<T extends {
8
+ [key: string]: any;
9
+ }>(client: T, guard: AccessGuard): T;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withAccess = withAccess;
4
+ /**
5
+ * Wraps a LocalFirstClient with an AccessGuard.
6
+ * Intercepts CRUD queries and mutations, asserting permissions before writes and
7
+ * filtering unauthorized rows or restricted fields on reads.
8
+ */
9
+ function withAccess(client, guard) {
10
+ return new Proxy(client, {
11
+ get(target, prop, receiver) {
12
+ if (prop === 'insert') {
13
+ return async (tableName, rowIdOrPayload, payloadOrOptions, options) => {
14
+ const payload = typeof rowIdOrPayload === 'string'
15
+ ? payloadOrOptions || {}
16
+ : rowIdOrPayload || {};
17
+ guard.assertCan('create', tableName, payload);
18
+ return target.insert(tableName, rowIdOrPayload, payloadOrOptions, options);
19
+ };
20
+ }
21
+ if (prop === 'update') {
22
+ return async (tableName, rowId, payload, options) => {
23
+ guard.assertCan('update', tableName, { id: rowId, ...payload });
24
+ return target.update(tableName, rowId, payload, options);
25
+ };
26
+ }
27
+ if (prop === 'delete') {
28
+ return async (tableName, rowId, options) => {
29
+ guard.assertCan('delete', tableName, { id: rowId });
30
+ return target.delete(tableName, rowId, options);
31
+ };
32
+ }
33
+ if (prop === 'findById') {
34
+ return async (tableName, id, options) => {
35
+ const record = await target.findById(tableName, id, options);
36
+ if (!record)
37
+ return null;
38
+ if (!guard.can('read', tableName, record)) {
39
+ return null;
40
+ }
41
+ return guard.filterAllowedFields('read', tableName, record, record);
42
+ };
43
+ }
44
+ if (prop === 'findMany') {
45
+ return async (tableName, options) => {
46
+ const records = await target.findMany(tableName, options);
47
+ return guard.filterRows('read', tableName, records);
48
+ };
49
+ }
50
+ if (prop === 'accessGuard') {
51
+ return guard;
52
+ }
53
+ return Reflect.get(target, prop, receiver);
54
+ },
55
+ });
56
+ }
@@ -0,0 +1,2 @@
1
+ import { AccessPolicyDefinition, AccessEngine } from './types.js';
2
+ export declare function defineAccessRules<TUser = any>(policy: AccessPolicyDefinition<TUser>): AccessEngine<TUser>;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defineAccessRules = defineAccessRules;
4
+ const guard_js_1 = require("./guard.js");
5
+ class RuleBuilderImpl {
6
+ rules = [];
7
+ addRule(inverted, actions, subjects, arg3, arg4) {
8
+ const actionList = Array.isArray(actions) ? actions : [actions];
9
+ const subjectList = Array.isArray(subjects) ? subjects : [subjects];
10
+ let options;
11
+ let predicate;
12
+ if (typeof arg3 === 'function') {
13
+ predicate = arg3;
14
+ }
15
+ else if (arg3 && typeof arg3 === 'object') {
16
+ options = arg3;
17
+ predicate = arg4;
18
+ }
19
+ this.rules.push({
20
+ inverted,
21
+ actions: actionList,
22
+ subjects: subjectList,
23
+ predicate,
24
+ fields: options?.fields,
25
+ description: options?.description,
26
+ });
27
+ return this;
28
+ }
29
+ can(action, subject, arg3, arg4) {
30
+ return this.addRule(false, action, subject, arg3, arg4);
31
+ }
32
+ cannot(action, subject, arg3, arg4) {
33
+ return this.addRule(true, action, subject, arg3, arg4);
34
+ }
35
+ }
36
+ function defineAccessRules(policy) {
37
+ const createGuard = (user) => {
38
+ const builder = new RuleBuilderImpl();
39
+ policy(builder, user);
40
+ return new guard_js_1.AccessGuard(user, builder.rules);
41
+ };
42
+ return {
43
+ createGuard,
44
+ for: createGuard,
45
+ };
46
+ }
@@ -0,0 +1,14 @@
1
+ export declare class AccessDeniedError extends Error {
2
+ readonly action: string;
3
+ readonly subject: string;
4
+ readonly fields?: string[];
5
+ readonly record?: any;
6
+ readonly reason?: string;
7
+ constructor(options: {
8
+ action: string;
9
+ subject: string;
10
+ fields?: string[];
11
+ record?: any;
12
+ reason?: string;
13
+ });
14
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AccessDeniedError = void 0;
4
+ class AccessDeniedError extends Error {
5
+ action;
6
+ subject;
7
+ fields;
8
+ record;
9
+ reason;
10
+ constructor(options) {
11
+ const fieldsText = options.fields && options.fields.length > 0 ? ` (fields: ${options.fields.join(', ')})` : '';
12
+ const message = options.reason ||
13
+ `Access denied: cannot perform action '${options.action}' on subject '${options.subject}'${fieldsText}.`;
14
+ super(message);
15
+ this.name = 'AccessDeniedError';
16
+ this.action = options.action;
17
+ this.subject = options.subject;
18
+ this.fields = options.fields;
19
+ this.record = options.record;
20
+ this.reason = options.reason;
21
+ }
22
+ }
23
+ exports.AccessDeniedError = AccessDeniedError;
@@ -0,0 +1,24 @@
1
+ import { AccessAction, AccessRule, AccessSubject } from './types.js';
2
+ export declare class AccessGuard<TUser = any> {
3
+ readonly user: TUser | null;
4
+ readonly rules: AccessRule<TUser>[];
5
+ constructor(user: TUser | null, rules: AccessRule<TUser>[]);
6
+ /**
7
+ * Evaluates if the current user has permission to perform an action on a subject.
8
+ */
9
+ can(action: AccessAction, subject: AccessSubject, record?: any): boolean;
10
+ cannot(action: AccessAction, subject: AccessSubject, record?: any): boolean;
11
+ assertCan(action: AccessAction, subject: AccessSubject, record?: any): void;
12
+ /**
13
+ * Check if a specific field is accessible for an action.
14
+ */
15
+ canAccessField(action: AccessAction, subject: AccessSubject, field: string, record?: any): boolean;
16
+ /**
17
+ * Strips restricted fields from an object based on rules.
18
+ */
19
+ filterAllowedFields<T extends Record<string, any>>(action: AccessAction, subject: AccessSubject, data: T, record?: any): Partial<T>;
20
+ /**
21
+ * Filters an array of rows, removing unauthorized records and stripping forbidden fields.
22
+ */
23
+ filterRows<T extends Record<string, any>>(action: AccessAction, subject: AccessSubject, rows: T[]): T[];
24
+ }
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AccessGuard = void 0;
4
+ const errors_js_1 = require("./errors.js");
5
+ class AccessGuard {
6
+ user;
7
+ rules;
8
+ constructor(user, rules) {
9
+ this.user = user;
10
+ this.rules = rules;
11
+ }
12
+ /**
13
+ * Evaluates if the current user has permission to perform an action on a subject.
14
+ */
15
+ can(action, subject, record) {
16
+ let allowed = false;
17
+ for (const rule of this.rules) {
18
+ const actionMatches = rule.actions.includes('manage') ||
19
+ rule.actions.includes('all') ||
20
+ rule.actions.includes(action);
21
+ const subjectMatches = rule.subjects.includes('all') ||
22
+ rule.subjects.includes(subject);
23
+ if (!actionMatches || !subjectMatches) {
24
+ continue;
25
+ }
26
+ if (rule.predicate) {
27
+ try {
28
+ if (!rule.predicate(record, this.user)) {
29
+ continue;
30
+ }
31
+ }
32
+ catch {
33
+ continue;
34
+ }
35
+ }
36
+ // If cannot rule matches (with no field filter), reject immediately
37
+ if (rule.inverted && (!rule.fields || rule.fields.length === 0)) {
38
+ return false;
39
+ }
40
+ // If can rule matches, mark allowed
41
+ if (!rule.inverted) {
42
+ allowed = true;
43
+ }
44
+ }
45
+ return allowed;
46
+ }
47
+ cannot(action, subject, record) {
48
+ return !this.can(action, subject, record);
49
+ }
50
+ assertCan(action, subject, record) {
51
+ if (!this.can(action, subject, record)) {
52
+ throw new errors_js_1.AccessDeniedError({ action, subject, record });
53
+ }
54
+ }
55
+ /**
56
+ * Check if a specific field is accessible for an action.
57
+ */
58
+ canAccessField(action, subject, field, record) {
59
+ if (!this.can(action, subject, record)) {
60
+ return false;
61
+ }
62
+ for (const rule of this.rules) {
63
+ if (!rule.inverted || !rule.fields || !rule.fields.includes(field)) {
64
+ continue;
65
+ }
66
+ const actionMatches = rule.actions.includes('manage') ||
67
+ rule.actions.includes('all') ||
68
+ rule.actions.includes(action);
69
+ const subjectMatches = rule.subjects.includes('all') ||
70
+ rule.subjects.includes(subject);
71
+ if (actionMatches && subjectMatches) {
72
+ if (!rule.predicate || rule.predicate(record, this.user)) {
73
+ return false;
74
+ }
75
+ }
76
+ }
77
+ return true;
78
+ }
79
+ /**
80
+ * Strips restricted fields from an object based on rules.
81
+ */
82
+ filterAllowedFields(action, subject, data, record) {
83
+ if (!data || typeof data !== 'object')
84
+ return data;
85
+ const result = {};
86
+ for (const [key, value] of Object.entries(data)) {
87
+ if (this.canAccessField(action, subject, key, record || data)) {
88
+ result[key] = value;
89
+ }
90
+ }
91
+ return result;
92
+ }
93
+ /**
94
+ * Filters an array of rows, removing unauthorized records and stripping forbidden fields.
95
+ */
96
+ filterRows(action, subject, rows) {
97
+ if (!Array.isArray(rows))
98
+ return [];
99
+ return rows
100
+ .filter((row) => this.can(action, subject, row))
101
+ .map((row) => this.filterAllowedFields(action, subject, row, row));
102
+ }
103
+ }
104
+ exports.AccessGuard = AccessGuard;
@@ -0,0 +1,5 @@
1
+ export * from './types.js';
2
+ export * from './errors.js';
3
+ export * from './guard.js';
4
+ export * from './engine.js';
5
+ export * from './client.js';
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types.js"), exports);
18
+ __exportStar(require("./errors.js"), exports);
19
+ __exportStar(require("./guard.js"), exports);
20
+ __exportStar(require("./engine.js"), exports);
21
+ __exportStar(require("./client.js"), exports);
@@ -0,0 +1,32 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { AccessGuard } from '../guard.js';
3
+ import { AccessAction, AccessSubject } from '../types.js';
4
+ export interface AccessContextValue<TUser = any> {
5
+ guard: AccessGuard<TUser> | null;
6
+ user: TUser | null;
7
+ can: (action: AccessAction, subject: AccessSubject, record?: any) => boolean;
8
+ cannot: (action: AccessAction, subject: AccessSubject, record?: any) => boolean;
9
+ assertCan: (action: AccessAction, subject: AccessSubject, record?: any) => void;
10
+ }
11
+ export interface AccessProviderProps<TUser = any> {
12
+ guard: AccessGuard<TUser>;
13
+ children: ReactNode;
14
+ }
15
+ export declare function AccessProvider<TUser = any>({ guard, children, }: AccessProviderProps<TUser>): React.JSX.Element;
16
+ export declare function useAccess<TUser = any>(): AccessContextValue<TUser>;
17
+ export interface CanProps {
18
+ I: AccessAction;
19
+ a: AccessSubject;
20
+ this?: any;
21
+ fallback?: ReactNode;
22
+ children: ReactNode;
23
+ }
24
+ export declare function Can({ I, a, this: record, fallback, children }: CanProps): React.JSX.Element;
25
+ export interface CannotProps {
26
+ I: AccessAction;
27
+ a: AccessSubject;
28
+ this?: any;
29
+ fallback?: ReactNode;
30
+ children: ReactNode;
31
+ }
32
+ export declare function Cannot({ I, a, this: record, fallback, children }: CannotProps): React.JSX.Element;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AccessProvider = AccessProvider;
4
+ exports.useAccess = useAccess;
5
+ exports.Can = Can;
6
+ exports.Cannot = Cannot;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = require("react");
9
+ const AccessContext = (0, react_1.createContext)(null);
10
+ function AccessProvider({ guard, children, }) {
11
+ const value = (0, react_1.useMemo)(() => {
12
+ return {
13
+ guard,
14
+ user: guard.user,
15
+ can: (action, subject, record) => guard.can(action, subject, record),
16
+ cannot: (action, subject, record) => guard.cannot(action, subject, record),
17
+ assertCan: (action, subject, record) => guard.assertCan(action, subject, record),
18
+ };
19
+ }, [guard]);
20
+ return (0, jsx_runtime_1.jsx)(AccessContext.Provider, { value: value, children: children });
21
+ }
22
+ function useAccess() {
23
+ const context = (0, react_1.useContext)(AccessContext);
24
+ if (!context) {
25
+ throw new Error('useAccess must be used within an <AccessProvider>');
26
+ }
27
+ return context;
28
+ }
29
+ function Can({ I, a, this: record, fallback = null, children }) {
30
+ const { can } = useAccess();
31
+ if (can(I, a, record)) {
32
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children });
33
+ }
34
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: fallback });
35
+ }
36
+ function Cannot({ I, a, this: record, fallback = null, children }) {
37
+ const { cannot } = useAccess();
38
+ if (cannot(I, a, record)) {
39
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children });
40
+ }
41
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: fallback });
42
+ }
@@ -0,0 +1,26 @@
1
+ export type AccessAction = 'create' | 'read' | 'update' | 'delete' | 'manage' | (string & {});
2
+ export type AccessSubject = 'all' | (string & {});
3
+ export type AccessPredicate<TRecord = any, TUser = any> = (record?: TRecord, user?: TUser | null) => boolean;
4
+ export interface RuleOptions {
5
+ fields?: string[];
6
+ description?: string;
7
+ }
8
+ export interface AccessRule<TUser = any> {
9
+ inverted: boolean;
10
+ actions: string[];
11
+ subjects: string[];
12
+ predicate?: AccessPredicate<any, TUser>;
13
+ fields?: string[];
14
+ description?: string;
15
+ }
16
+ export interface AccessRuleBuilder<TUser = any> {
17
+ can(action: AccessAction | AccessAction[], subject: AccessSubject | AccessSubject[], predicate?: AccessPredicate<any, TUser>): this;
18
+ can(action: AccessAction | AccessAction[], subject: AccessSubject | AccessSubject[], options: RuleOptions, predicate?: AccessPredicate<any, TUser>): this;
19
+ cannot(action: AccessAction | AccessAction[], subject: AccessSubject | AccessSubject[], predicate?: AccessPredicate<any, TUser>): this;
20
+ cannot(action: AccessAction | AccessAction[], subject: AccessSubject | AccessSubject[], options: RuleOptions, predicate?: AccessPredicate<any, TUser>): this;
21
+ }
22
+ export type AccessPolicyDefinition<TUser = any> = (builder: AccessRuleBuilder<TUser>, user: TUser | null) => void;
23
+ export interface AccessEngine<TUser = any> {
24
+ createGuard(user: TUser | null): any;
25
+ for(user: TUser | null): any;
26
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -7,6 +7,7 @@ import { SchemaManager } from './schema-manager.js';
7
7
  import { BatchMutation, ConflictResolver, ConflictStrategy, MutateOptions, NetworkStatusProvider, OutboxRecord, OutboxStatus, QueryOptions, RetryConfig, SyncMetrics, SyncOp, TableChangeEvent, WhereFilter } from './types.js';
8
8
  import { SyncLogger, CustomLogger, LogLevel } from '../utils/logger.js';
9
9
  import { QueryBuilder, AggregateSpec, AggregateResult } from './query-builder.js';
10
+ import { type ExportJsonOptions, type ImportJsonOptions, type ExportCsvOptions, type ImportCsvOptions, type ImportSummary } from '../utils/portability.js';
10
11
  export interface LocalFirstClientConfig {
11
12
  local: LocalDBAdapter;
12
13
  remote: RemoteDBAdapter;
@@ -62,6 +63,10 @@ export declare class TransactionalClient {
62
63
  includeDeleted?: boolean;
63
64
  }): Promise<T[]>;
64
65
  findMany<T = any>(tableName: string, options?: QueryOptions): Promise<T[]>;
66
+ /**
67
+ * Alias for findMany().
68
+ */
69
+ find<T = any>(tableName: string, options?: QueryOptions): Promise<T[]>;
65
70
  query<T = any>(sql: string, params?: any[]): Promise<T[]>;
66
71
  from<T = any>(tableName: string): QueryBuilder<T>;
67
72
  get rawTx(): LocalTransaction;
@@ -227,6 +232,32 @@ export declare class LocalFirstClient {
227
232
  */
228
233
  aggregate(tableName: string, spec: AggregateSpec): Promise<AggregateResult>;
229
234
  findMany<T = any>(tableName: string, options?: QueryOptions): Promise<T[]>;
235
+ /**
236
+ * Alias for findMany().
237
+ */
238
+ find<T = any>(tableName: string, options?: QueryOptions): Promise<T[]>;
239
+ /**
240
+ * Returns true if client storage has successfully initialized.
241
+ */
242
+ get isReady(): boolean;
243
+ /**
244
+ * Convenience export of tables to JSON.
245
+ */
246
+ exportToJson(tables?: string[], options?: ExportJsonOptions): Promise<string>;
247
+ /**
248
+ * Convenience import of tables from JSON.
249
+ */
250
+ importFromJson(jsonString: string, options?: ImportJsonOptions): Promise<ImportSummary>;
251
+ /**
252
+ * Convenience export of a table to RFC-4180 CSV.
253
+ */
254
+ exportToCsv(tableName: string, options?: ExportCsvOptions): Promise<string>;
255
+ /**
256
+ * Convenience import of RFC-4180 CSV into a table.
257
+ */
258
+ importFromCsv(tableName: string, csvString: string, options?: ImportCsvOptions): Promise<{
259
+ rowCount: number;
260
+ }>;
230
261
  /**
231
262
  * Finds a single record by primary key.
232
263
  *
@@ -12,6 +12,7 @@ const uuid_js_1 = require("../utils/uuid.js");
12
12
  const sql_js_1 = require("../utils/sql.js");
13
13
  const query_js_1 = require("./query.js");
14
14
  const query_builder_js_1 = require("./query-builder.js");
15
+ const portability_js_1 = require("../utils/portability.js");
15
16
  function hasFunctionFilter(value) {
16
17
  if (typeof value === 'function')
17
18
  return true;
@@ -133,6 +134,12 @@ class TransactionalClient {
133
134
  }
134
135
  return this.from(tableName).applyOptions(options).execute();
135
136
  }
137
+ /**
138
+ * Alias for findMany().
139
+ */
140
+ async find(tableName, options) {
141
+ return this.findMany(tableName, options);
142
+ }
136
143
  async query(sql, params = []) {
137
144
  return (await this.tx.execute(sql, params));
138
145
  }
@@ -715,6 +722,42 @@ class LocalFirstClient {
715
722
  }
716
723
  return this.from(tableName).applyOptions(options).execute();
717
724
  }
725
+ /**
726
+ * Alias for findMany().
727
+ */
728
+ async find(tableName, options) {
729
+ return this.findMany(tableName, options);
730
+ }
731
+ /**
732
+ * Returns true if client storage has successfully initialized.
733
+ */
734
+ get isReady() {
735
+ return Boolean(this.initialization);
736
+ }
737
+ /**
738
+ * Convenience export of tables to JSON.
739
+ */
740
+ async exportToJson(tables, options) {
741
+ return (0, portability_js_1.exportTablesToJson)(this, tables, options);
742
+ }
743
+ /**
744
+ * Convenience import of tables from JSON.
745
+ */
746
+ async importFromJson(jsonString, options) {
747
+ return (0, portability_js_1.importTablesFromJson)(this, jsonString, options);
748
+ }
749
+ /**
750
+ * Convenience export of a table to RFC-4180 CSV.
751
+ */
752
+ async exportToCsv(tableName, options) {
753
+ return (0, portability_js_1.exportTableToCsv)(this, tableName, options);
754
+ }
755
+ /**
756
+ * Convenience import of RFC-4180 CSV into a table.
757
+ */
758
+ async importFromCsv(tableName, csvString, options) {
759
+ return (0, portability_js_1.importTableFromCsv)(this, tableName, csvString, options);
760
+ }
718
761
  /**
719
762
  * Finds a single record by primary key.
720
763
  *
@@ -8,6 +8,7 @@ export * from './meta-store.js';
8
8
  export * from './table-manager.js';
9
9
  export * from './schema-manager.js';
10
10
  export * from './query-builder.js';
11
+ export * from './maintenance.js';
11
12
  export * from '../reactive/event-emitter.js';
12
13
  export * from '../adapters/network/index.js';
13
14
  export type { LocalDBAdapter, LocalTransaction } from '../adapters/local/index.js';
@@ -25,6 +25,7 @@ __exportStar(require("./meta-store.js"), exports);
25
25
  __exportStar(require("./table-manager.js"), exports);
26
26
  __exportStar(require("./schema-manager.js"), exports);
27
27
  __exportStar(require("./query-builder.js"), exports);
28
+ __exportStar(require("./maintenance.js"), exports);
28
29
  __exportStar(require("../reactive/event-emitter.js"), exports);
29
30
  __exportStar(require("../adapters/network/index.js"), exports);
30
31
  __exportStar(require("../utils/logger.js"), exports);
@@ -7,6 +7,7 @@ const outbox_store_js_1 = require("./outbox-store.js");
7
7
  const meta_store_js_1 = require("./meta-store.js");
8
8
  const table_manager_js_1 = require("./table-manager.js");
9
9
  const logger_js_1 = require("../utils/logger.js");
10
+ const dynamic_js_1 = require("../utils/dynamic.js");
10
11
  const REACT_NATIVE_PKG = ['react', 'native'].join('-');
11
12
  /** Maximum outbox records sent per push cycle. */
12
13
  const PUSH_BATCH_SIZE = 50;
@@ -236,7 +237,8 @@ class SyncWorker {
236
237
  return false;
237
238
  // React Native AppState
238
239
  try {
239
- const { AppState } = require('react-native');
240
+ const rn = (0, dynamic_js_1.safeDynamicRequire)(REACT_NATIVE_PKG);
241
+ const AppState = rn?.AppState;
240
242
  if (AppState?.currentState && typeof AppState.currentState === 'string') {
241
243
  return Boolean(AppState.currentState.match(/inactive|background/));
242
244
  }
@@ -255,7 +257,8 @@ class SyncWorker {
255
257
  return;
256
258
  // 1. Try React Native AppState
257
259
  try {
258
- const { AppState } = require('react-native');
260
+ const rn = (0, dynamic_js_1.safeDynamicRequire)(REACT_NATIVE_PKG);
261
+ const AppState = rn?.AppState;
259
262
  if (AppState && typeof AppState.addEventListener === 'function') {
260
263
  const sub = AppState.addEventListener('change', (state) => {
261
264
  if (state.match(/inactive|background/)) {
package/dist/index.d.ts CHANGED
@@ -22,3 +22,4 @@ export * from './utils/portability.js';
22
22
  export * from './schema/index.js';
23
23
  export * from './auth/index.js';
24
24
  export * from './backups/index.js';
25
+ export * from './access/index.js';
package/dist/index.js CHANGED
@@ -37,3 +37,4 @@ __exportStar(require("./utils/portability.js"), exports);
37
37
  __exportStar(require("./schema/index.js"), exports);
38
38
  __exportStar(require("./auth/index.js"), exports);
39
39
  __exportStar(require("./backups/index.js"), exports);
40
+ __exportStar(require("./access/index.js"), exports);
@@ -111,6 +111,18 @@ export interface UseMutationsResult<TTable extends string = string> {
111
111
  * Universal mutations hook providing memoized, null-safe helpers for database operations.
112
112
  */
113
113
  export declare function useMutations<TTable extends string = string>(client: LocalFirstClient | null | undefined): UseMutationsResult<TTable>;
114
+ export interface UseDatabaseMaintenanceResult {
115
+ stats: import('../core/maintenance.js').StorageStats | null;
116
+ loading: boolean;
117
+ refreshStats: () => Promise<import('../core/maintenance.js').StorageStats | null>;
118
+ pruneOutbox: (options?: import('../core/maintenance.js').PruneOutboxOptions) => Promise<number>;
119
+ vacuum: () => Promise<void>;
120
+ checkpoint: (mode?: import('../core/maintenance.js').CheckpointMode) => Promise<void>;
121
+ }
122
+ /**
123
+ * Reactive hook for database maintenance (vacuum, WAL checkpoint, outbox pruning, and storage metrics).
124
+ */
125
+ export declare function useDatabaseMaintenance(client: LocalFirstClient | null | undefined): UseDatabaseMaintenanceResult;
114
126
  export * from './runner.js';
115
127
  export * from './status.js';
116
128
  export * from './theme.js';
@@ -21,6 +21,7 @@ exports.useLiveQueries = useLiveQueries;
21
21
  exports.useLiveRow = useLiveRow;
22
22
  exports.useLiveInfiniteQuery = useLiveInfiniteQuery;
23
23
  exports.useMutations = useMutations;
24
+ exports.useDatabaseMaintenance = useDatabaseMaintenance;
24
25
  const react_1 = require("react");
25
26
  const runner_js_1 = require("./runner.js");
26
27
  function areRowsEqual(a, b) {
@@ -587,6 +588,56 @@ function useMutations(client) {
587
588
  transaction: (action) => client ? client.transaction(action) : Promise.resolve(undefined),
588
589
  }), [client]);
589
590
  }
591
+ /**
592
+ * Reactive hook for database maintenance (vacuum, WAL checkpoint, outbox pruning, and storage metrics).
593
+ */
594
+ function useDatabaseMaintenance(client) {
595
+ const [stats, setStats] = (0, react_1.useState)(null);
596
+ const [loading, setLoading] = (0, react_1.useState)(false);
597
+ const refreshStats = (0, react_1.useCallback)(async () => {
598
+ if (!client)
599
+ return null;
600
+ setLoading(true);
601
+ try {
602
+ const res = await client.maintenance.getStorageStats();
603
+ setStats(res);
604
+ return res;
605
+ }
606
+ catch {
607
+ return null;
608
+ }
609
+ finally {
610
+ setLoading(false);
611
+ }
612
+ }, [client]);
613
+ const pruneOutbox = (0, react_1.useCallback)(async (options) => {
614
+ if (!client)
615
+ return 0;
616
+ const count = await client.maintenance.pruneOutbox(options);
617
+ await refreshStats();
618
+ return count;
619
+ }, [client, refreshStats]);
620
+ const vacuum = (0, react_1.useCallback)(async () => {
621
+ if (!client)
622
+ return;
623
+ await client.maintenance.vacuum();
624
+ await refreshStats();
625
+ }, [client, refreshStats]);
626
+ const checkpoint = (0, react_1.useCallback)(async (mode) => {
627
+ if (!client)
628
+ return;
629
+ await client.maintenance.checkpoint(mode);
630
+ await refreshStats();
631
+ }, [client, refreshStats]);
632
+ return {
633
+ stats,
634
+ loading,
635
+ refreshStats,
636
+ pruneOutbox,
637
+ vacuum,
638
+ checkpoint,
639
+ };
640
+ }
590
641
  __exportStar(require("./runner.js"), exports);
591
642
  __exportStar(require("./status.js"), exports);
592
643
  __exportStar(require("./theme.js"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-lite",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "description": "Never let a lost connection break a promise. Local-first sync that faithfully keeps data safe until you're back online.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -56,6 +56,16 @@
56
56
  "import": "./dist/backups/index.js",
57
57
  "default": "./dist/backups/index.js"
58
58
  },
59
+ "./portability": {
60
+ "types": "./dist/utils/portability.d.ts",
61
+ "import": "./dist/utils/portability.js",
62
+ "default": "./dist/utils/portability.js"
63
+ },
64
+ "./maintenance": {
65
+ "types": "./dist/core/maintenance.d.ts",
66
+ "import": "./dist/core/maintenance.js",
67
+ "default": "./dist/core/maintenance.js"
68
+ },
59
69
  "./expo-sqlite": {
60
70
  "types": "./dist/adapters/local/expo-sqlite.d.ts",
61
71
  "import": "./dist/adapters/local/expo-sqlite.js",
@@ -90,6 +100,16 @@
90
100
  "types": "./dist/core/index.d.ts",
91
101
  "import": "./dist/core/index.js",
92
102
  "default": "./dist/core/index.js"
103
+ },
104
+ "./access": {
105
+ "types": "./dist/access/index.d.ts",
106
+ "import": "./dist/access/index.js",
107
+ "default": "./dist/access/index.js"
108
+ },
109
+ "./access/react": {
110
+ "types": "./dist/access/react/index.d.ts",
111
+ "import": "./dist/access/react/index.js",
112
+ "default": "./dist/access/react/index.js"
93
113
  }
94
114
  },
95
115
  "scripts": {