ofsync-shared-core 0.2.0-alpha.0 → 0.2.0-alpha.1
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.
- package/dist/OfsyncCore.d.ts +6 -0
- package/dist/adapters/OfsyncHttpTransport.d.ts +10 -1
- package/dist/auth/OfsyncAuthClient.d.ts +43 -0
- package/dist/contracts/SyncContract.d.ts +9 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +1 -1
- package/dist/index.js +1 -1
- package/dist/services/InMemoryOutboxStore.d.ts +2 -0
- package/dist/services/SyncTransport.d.ts +2 -0
- package/package.json +2 -2
package/dist/OfsyncCore.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface OfsyncRuntimeState {
|
|
|
16
16
|
lastError: string | null;
|
|
17
17
|
lastTransitionAt: string;
|
|
18
18
|
}
|
|
19
|
+
export interface OfsyncOutboxState {
|
|
20
|
+
pending: number;
|
|
21
|
+
failed: number;
|
|
22
|
+
total: number;
|
|
23
|
+
}
|
|
19
24
|
export interface OfsyncCoreOptions {
|
|
20
25
|
policy?: Partial<SyncPolicy>;
|
|
21
26
|
transport?: SyncTransport;
|
|
@@ -42,6 +47,7 @@ export declare class OfsyncCore {
|
|
|
42
47
|
getPolicy(): SyncPolicy;
|
|
43
48
|
registerDomainBridge(bridge: DomainBridge): void;
|
|
44
49
|
listDomainBridges(): string[];
|
|
50
|
+
getOutboxState(): Promise<OfsyncOutboxState>;
|
|
45
51
|
enqueueLocalChange(change: SyncChange, idempotencyKey?: string): Promise<OutboxItem>;
|
|
46
52
|
start(options?: CoreRuntimeStartOptions): Promise<void>;
|
|
47
53
|
stop(): Promise<void>;
|
|
@@ -6,28 +6,37 @@ export interface OfsyncHttpTransportOptions {
|
|
|
6
6
|
httpAdapter: HttpAdapter;
|
|
7
7
|
getAccessToken?: () => string | null | undefined | Promise<string | null | undefined>;
|
|
8
8
|
nowIso?: () => string;
|
|
9
|
+
allowGenericFallback?: boolean;
|
|
9
10
|
}
|
|
10
11
|
export declare class OfsyncTransportError extends Error {
|
|
11
12
|
readonly code: string;
|
|
12
13
|
readonly retryable: boolean;
|
|
13
14
|
readonly status?: number;
|
|
14
|
-
|
|
15
|
+
readonly details?: Record<string, unknown>;
|
|
16
|
+
constructor(code: string, message: string, retryable?: boolean, status?: number, details?: Record<string, unknown>);
|
|
15
17
|
}
|
|
16
18
|
export declare class OfsyncHttpTransport implements SyncTransport {
|
|
17
19
|
private readonly options;
|
|
18
20
|
constructor(options: OfsyncHttpTransportOptions);
|
|
19
21
|
private nowIso;
|
|
22
|
+
private defaultBootstrapSinceIso;
|
|
20
23
|
private buildHeaders;
|
|
21
24
|
private normalizeUrl;
|
|
25
|
+
private normalizeDomainPath;
|
|
26
|
+
private resolveDomainPathOrThrow;
|
|
27
|
+
private isGenericFallbackEnabled;
|
|
28
|
+
private requestWithDomainPolicy;
|
|
22
29
|
private parseFailure;
|
|
23
30
|
push(params: {
|
|
24
31
|
scope: SyncScope;
|
|
25
32
|
outbox: OutboxItem[];
|
|
33
|
+
domain: string;
|
|
26
34
|
}): Promise<SyncPushResult>;
|
|
27
35
|
pull(params: {
|
|
28
36
|
scope: SyncScope;
|
|
29
37
|
cursor: string | null;
|
|
30
38
|
limit: number;
|
|
39
|
+
domain: string;
|
|
31
40
|
}): Promise<SyncPullResult>;
|
|
32
41
|
listConflicts(params: {
|
|
33
42
|
scope: SyncScope;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface OfsyncScopeOption {
|
|
2
|
+
tenantId: string | null;
|
|
3
|
+
branchId: string;
|
|
4
|
+
role: string;
|
|
5
|
+
userId: string;
|
|
6
|
+
email: string;
|
|
7
|
+
}
|
|
8
|
+
export interface OfsyncAuthSession {
|
|
9
|
+
accessToken: string;
|
|
10
|
+
refreshToken: string;
|
|
11
|
+
expiresAtMs: number;
|
|
12
|
+
principal: string;
|
|
13
|
+
}
|
|
14
|
+
export interface OfsyncAuthClientOptions {
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
deviceId: string;
|
|
17
|
+
fetchImpl?: typeof fetch;
|
|
18
|
+
accessTokenFallbackTtlMs?: number;
|
|
19
|
+
networkErrorMessage?: (baseUrl: string) => string;
|
|
20
|
+
}
|
|
21
|
+
export interface OfsyncScopeOverride {
|
|
22
|
+
tenantId?: string | null;
|
|
23
|
+
branchId?: string | null;
|
|
24
|
+
}
|
|
25
|
+
export interface ResolveSyncAccessTokenInput {
|
|
26
|
+
bearerToken: string;
|
|
27
|
+
session: OfsyncAuthSession | null;
|
|
28
|
+
nowMs?: number;
|
|
29
|
+
refreshLeewayMs?: number;
|
|
30
|
+
refresh: () => Promise<OfsyncAuthSession | null>;
|
|
31
|
+
loginFromCredentials: () => Promise<OfsyncAuthSession | null>;
|
|
32
|
+
}
|
|
33
|
+
export declare function parseSyncDurationMs(raw: unknown, fallbackMs?: number): number;
|
|
34
|
+
export declare function normalizeSyncNetworkError(error: unknown, baseUrl: string, messageBuilder?: (baseUrl: string) => string): string;
|
|
35
|
+
export declare function createSyncAuthError(code: string, message: string): Error & {
|
|
36
|
+
code: string;
|
|
37
|
+
};
|
|
38
|
+
export declare function resolveSyncAccessToken(input: ResolveSyncAccessTokenInput): Promise<string>;
|
|
39
|
+
export declare function createOfsyncAuthClient(options: OfsyncAuthClientOptions): {
|
|
40
|
+
fetchScopeOptions(username: string, password: string): Promise<OfsyncScopeOption[]>;
|
|
41
|
+
login(username: string, password: string, scope: OfsyncScopeOverride): Promise<OfsyncAuthSession>;
|
|
42
|
+
refresh(refreshToken: string, tenantId?: string): Promise<OfsyncAuthSession>;
|
|
43
|
+
};
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { ScopeRef } from 'ofcore';
|
|
2
|
+
/**
|
|
3
|
+
* Sync scope — extends ScopeRef dari ofcore.
|
|
4
|
+
* Tambahan: `deviceId` untuk multi-device offline sync,
|
|
5
|
+
* `ledgerProfileId` untuk isolasi buku besar lintas-branch (finance use-case).
|
|
6
|
+
*/
|
|
7
|
+
export interface SyncScope extends ScopeRef {
|
|
4
8
|
deviceId?: string;
|
|
9
|
+
/** ID ledger profile untuk isolasi finance journal lintas branch. */
|
|
10
|
+
ledgerProfileId?: string;
|
|
5
11
|
}
|
|
6
12
|
export type SyncChangeType = 'CREATE' | 'UPDATE' | 'DELETE';
|
|
7
13
|
export interface SyncChange {
|
package/dist/index.d.ts
CHANGED
package/dist/index.esm.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var d={batchSize:200,maxRetry:5,scheduler:{mode:"manual",onlineTrigger:!0},retryBackoff:{strategy:"exponential-jitter",baseDelayMs:500,maxDelayMs:3e4}};var y=class{constructor(){this.rows=new Map}async enqueue(t){this.rows.set(t.outboxId,t)}async listPending(t){return[...this.rows.values()].filter(e=>e.status==="PENDING"||e.status==="FAILED").sort((e,n)=>e.createdAt.localeCompare(n.createdAt)).slice(0,Math.max(0,t))}async findByIdempotencyKey(t){for(let e of this.rows.values())if(e.idempotencyKey===t)return e;return null}async update(t){this.rows.set(t.outboxId,t)}async remove(t){this.rows.delete(t)}};var h=class{constructor(){this.rows=new Map}async get(t){return this.rows.has(t)?this.rows.get(t):null}async set(t,e){this.rows.set(t,e)}};function f(r){return r.code==="VERSION_CONFLICT"?{conflictId:r.conflictId,action:"APPLY_REMOTE"}:r.code==="SCOPE_CONFLICT"?{conflictId:r.conflictId,action:"MANUAL_MERGE"}:{conflictId:r.conflictId,action:"KEEP_LOCAL"}}var _="phase1-runtime-skeleton";import{CoreRuntime as C}from"ofcore";import{InMemoryDbAdapter as I}from"ofcore";import{asReadonlyStore as w,createStore as v}from"ofcore";function x(r={}){return{...d,...r,scheduler:{...d.scheduler,...r.scheduler||{}},retryBackoff:{...d.retryBackoff,...r.retryBackoff||{}}}}function A(r){return`${r.tenantId||"__standalone__"}::${r.branchId||"__all__"}::${r.deviceId||"__device__"}`}var u=class{constructor(t,e,n={}){this.runtime=t;this.runtimeStateStore=e;this.bridges=new Map;this.runtimeStore=w(this.runtimeStateStore),this.policy=x(n.policy),this.transport=n.transport,this.outboxStore=n.outboxStore||new y,this.checkpointStore=n.checkpointStore||new h,this.conflictResolvers=n.conflictResolvers||[],this.projectionHook=n.projectionHook}static builder(){return new S}get registry(){return this.runtime.registry}getPolicy(){return this.policy}registerDomainBridge(t){let e=t.domain.trim();if(!e)throw new Error("DomainBridge.domain is required");if(this.bridges.has(e))throw new Error(`DomainBridge for domain "${e}" is already registered`);this.bridges.set(e,t)}listDomainBridges(){return[...this.bridges.keys()].sort()}async enqueueLocalChange(t,e){let n=(e||`${t.domain}:${t.entity}:${t.recordId}:${t.type}:${t.id}`).trim();if(!n)throw new Error("idempotencyKey must not be empty");let i=await this.outboxStore.findByIdempotencyKey(n);if(i)return i;let c=new Date().toISOString(),s={outboxId:`outbox_${Date.now()}_${Math.random().toString(36).slice(2)}`,idempotencyKey:n,change:t,retryCount:0,status:"PENDING",createdAt:c,updatedAt:c};return await this.outboxStore.enqueue(s),s}async start(t={}){this.runtimeStateStore.setState({phase:"starting",started:!1,syncing:!1,lastError:null,lastTransitionAt:new Date().toISOString()});try{await this.runtime.start(t),this.runtimeStateStore.setState(e=>({...e,phase:"started",started:!0,syncing:!1,startCount:e.startCount+1,lastError:null,lastTransitionAt:new Date().toISOString()}))}catch(e){throw this.runtimeStateStore.setState({phase:"error",started:!1,syncing:!1,lastError:e instanceof Error?e.message:String(e),lastTransitionAt:new Date().toISOString()}),e}}async stop(){this.stopScheduler(),this.runtimeStateStore.setState({phase:"stopping",started:this.runtime.isStarted(),syncing:!1,lastError:null,lastTransitionAt:new Date().toISOString()});try{await this.runtime.stop(),this.runtimeStateStore.setState(t=>({...t,phase:"stopped",started:!1,syncing:!1,stopCount:t.stopCount+1,lastError:null,lastTransitionAt:new Date().toISOString()}))}catch(t){throw this.runtimeStateStore.setState({phase:"error",started:this.runtime.isStarted(),syncing:!1,lastError:t instanceof Error?t.message:String(t),lastTransitionAt:new Date().toISOString()}),t}}async startSync(t){if(!this.runtime.isStarted())throw new Error("Cannot start sync before runtime is started");this.runtimeStateStore.setState({phase:"syncing",started:!0,syncing:!0,lastError:null,lastTransitionAt:new Date().toISOString()});try{let e=A(t),n=await this.checkpointStore.get(e);if(this.transport){await this.processOutbox(t);let i=await this.transport.pull({scope:t,cursor:n,limit:this.policy.batchSize});await this.applyRemoteChanges(i.changes||[],t),i.nextCursor!==void 0&&await this.checkpointStore.set(e,i.nextCursor)}this.runtimeStateStore.setState(i=>({...i,phase:"started",started:!0,syncing:!1,syncCount:i.syncCount+1,lastError:null,lastTransitionAt:new Date().toISOString()}))}catch(e){throw this.runtimeStateStore.setState({phase:"error",started:this.runtime.isStarted(),syncing:!1,lastError:e instanceof Error?e.message:String(e),lastTransitionAt:new Date().toISOString()}),e}}async stopSync(){this.runtimeStateStore.setState(t=>({...t,phase:this.runtime.isStarted()?"started":"stopped",syncing:!1,lastTransitionAt:new Date().toISOString()}))}isStarted(){return this.runtime.isStarted()}startScheduler(t){if(this.policy.scheduler.mode!=="interval")return;let e=this.policy.scheduler.intervalMs||1e4;this.stopScheduler(),this.schedulerTimer=setInterval(()=>{this.startSync(t).catch(n=>{this.runtimeStateStore.setState(i=>({...i,phase:"error",syncing:!1,lastError:n instanceof Error?n.message:String(n),lastTransitionAt:new Date().toISOString()}))})},e)}stopScheduler(){this.schedulerTimer&&(clearInterval(this.schedulerTimer),this.schedulerTimer=void 0)}async notifyOnline(t){this.policy.scheduler.onlineTrigger!==!1&&await this.startSync(t)}async processOutbox(t){if(!this.transport)return;let e=await this.outboxStore.listPending(this.policy.batchSize);if(e.length===0)return;let n=await this.transport.push({scope:t,outbox:e}),i=new Set((n.applied||[]).map(o=>o.changeId)),c=new Map((n.failed||[]).map(o=>[o.changeId,o])),s=new Set((n.conflicts||[]).map(o=>o.changeId));await this.resolveConflicts(n.conflicts||[],t);for(let o of e){if(i.has(o.change.id)){await this.outboxStore.remove(o.outboxId);continue}let a=c.get(o.change.id);if(a||s.has(o.change.id)){let l=o.retryCount+1;await this.outboxStore.update({...o,retryCount:l,status:l>=this.policy.maxRetry?"FAILED":"PENDING",lastError:(a==null?void 0:a.message)||(s.has(o.change.id)?"SYNC_CONFLICT":o.lastError),updatedAt:new Date().toISOString()})}}}async applyRemoteChanges(t,e){if(!Array.isArray(t)||t.length===0)return;let n=[...t].sort((c,s)=>c.occurredAt.localeCompare(s.occurredAt)),i=new Map;for(let c of n){i.has(c.domain)||i.set(c.domain,[]);let s=i.get(c.domain);s&&s.push(c)}for(let[c,s]of i.entries()){let o=this.bridges.get(c);if(o!=null&&o.applyRemoteChanges&&(await o.applyRemoteChanges(s,e),this.projectionHook))for(let a of s)await this.projectionHook(a,e)}}async resolveConflicts(t,e){var i;if(!((i=this.transport)!=null&&i.resolveConflicts)||t.length===0)return;let n=[];for(let c of t){let s=[...this.conflictResolvers],o=null;for(let a of s){let l=await a(c,{scope:e});if(l){o=l;break}}o||(o=f(c)),n.push(o)}n.length>0&&await this.transport.resolveConflicts({scope:e,resolutions:n})}},S=class{constructor(){this.runtimeBuilder=C.builder();this.options={};this.runtimeBuilder.withDbAdapter(()=>new I)}withPlatformAdapter(t){return this.runtimeBuilder.withPlatformAdapter(t),this}withDbAdapter(t){return this.runtimeBuilder.withDbAdapter(t),this}withHttpAdapter(t){return this.runtimeBuilder.withHttpAdapter(t),this}withSocketAdapter(t){return this.runtimeBuilder.withSocketAdapter(t),this}withLoggerAdapter(t){return this.runtimeBuilder.withLoggerAdapter(t),this}withActivitySink(t){return this.runtimeBuilder.withActivitySink(t),this}withExtension(t,e){return this.runtimeBuilder.withExtension(t,e),this}withPolicy(t){return this.options={...this.options,policy:t},this}withTransport(t){return this.options={...this.options,transport:t},this}withOutboxStore(t){return this.options={...this.options,outboxStore:t},this}withCheckpointStore(t){return this.options={...this.options,checkpointStore:t},this}withConflictResolvers(t){return this.options={...this.options,conflictResolvers:t},this}withProjectionHook(t){return this.options={...this.options,projectionHook:t},this}build(){let t=v({phase:"idle",started:!1,syncing:!1,startCount:0,stopCount:0,syncCount:0,lastError:null,lastTransitionAt:new Date().toISOString()}),e=this.runtimeBuilder.build();return new u(e,t,this.options)}};function q(r={}){let t=u.builder();return r.policy&&t.withPolicy(r.policy),r.transport&&t.withTransport(r.transport),r.outboxStore&&t.withOutboxStore(r.outboxStore),r.checkpointStore&&t.withCheckpointStore(r.checkpointStore),r.conflictResolvers&&t.withConflictResolvers(r.conflictResolvers),r.projectionHook&&t.withProjectionHook(r.projectionHook),t.build()}function G(r,t){r.registerDomainBridge(t)}async function z(r,t,e={}){r.isStarted()||await r.start(e),await r.startSync(t)}async function Y(r){await r.stopSync()}var m=class extends Error{constructor(t,e,n=!1,i){super(e),this.name="OfsyncTransportError",this.code=t,this.retryable=n,this.status=i}};async function R(r){let t=globalThis.crypto;if(!t||!t.subtle)throw new Error("WebCrypto API is not available");let e=new TextEncoder().encode(r),n=await t.subtle.digest("SHA-256",e);return[...new Uint8Array(n)].map(i=>i.toString(16).padStart(2,"0")).join("")}function O(r){let t=String(r.table||""),e=String(r.id||""),n=String(r.changeId||"");return[t,e,n].join("::")}function g(r){return{conflictId:O(r),changeId:String(r.changeId||""),domain:"unknown",entity:String(r.table||""),recordId:String(r.id||""),code:String(r.code||"UNKNOWN_CONFLICT"),message:String(r.message||"Conflict detected"),retryable:!!r.retryable,scope:{tenantId:typeof r.tenantId=="string"?r.tenantId:void 0,branchId:typeof r.branchId=="string"?r.branchId:void 0},details:r}}function P(r){var e;let t=(e=r.branchId)==null?void 0:e.trim();if(!t)throw new m("BRANCH_SCOPE_REQUIRED","Sync scope requires branchId",!1);return t}var b=class{constructor(t){this.options=t}nowIso(){return this.options.nowIso?this.options.nowIso():new Date().toISOString()}async buildHeaders(t){let e={"content-type":"application/json","x-branch-id":P(t)};if(t.tenantId&&(e["x-tenant-id"]=t.tenantId),this.options.getAccessToken){let n=await this.options.getAccessToken();n&&(e.authorization=`Bearer ${n}`)}return e}normalizeUrl(t){return`${this.options.baseUrl.replace(/\/+$/,"")}${t}`}parseFailure(t,e){var o,a,l;let n=e||{},i=((o=n.error)==null?void 0:o.code)||"SYNC_TRANSPORT_ERROR",c=((a=n.error)==null?void 0:a.message)||`Sync transport failed with status ${t}`,s=!!((l=n.error)!=null&&l.retryable);throw new m(i,c,s,t)}async push(t){let e={changes:t.outbox.map(a=>({id:a.change.id,entity:a.change.entity,type:a.change.type,data:{id:a.change.recordId,...a.change.payload},timestamp:a.change.occurredAt}))},n=JSON.stringify(e),i=await R(n),c=await this.buildHeaders(t.scope);c["x-data-checksum"]=i;let s=await this.options.httpAdapter.request({method:"POST",url:this.normalizeUrl("/sync/push"),headers:c,body:e});s.status>=400&&this.parseFailure(s.status,s.data);let o=s.data||{};return{applied:Array.isArray(o.applied)?o.applied:[],failed:Array.isArray(o.failed)?o.failed:[],conflicts:Array.isArray(o.conflicts)?o.conflicts.map(g):[]}}async pull(t){let e=await this.buildHeaders(t.scope),n=t.cursor||this.nowIso(),i=await this.options.httpAdapter.request({method:"GET",url:this.normalizeUrl("/sync/pull"),headers:e,query:{since:n}});i.status>=400&&this.parseFailure(i.status,i.data);let c=i.data||{},s=Array.isArray(c.changes)?c.changes.map(a=>{var l;return{id:String(a.id||""),domain:String(a.domain||"unknown"),entity:String(a.entity||""),type:String(a.type||"UPDATE"),recordId:String(a.recordId||((l=a.record)==null?void 0:l.id)||""),payload:a.record||{},occurredAt:String(a.timestamp||this.nowIso()),scope:t.scope}}):[],o=s.length>0?s[s.length-1].occurredAt:n;return{changes:s,nextCursor:o,conflicts:[]}}async listConflicts(t){let e=await this.buildHeaders(t.scope),n=await this.options.httpAdapter.request({method:"GET",url:this.normalizeUrl("/sync/conflicts"),headers:e});n.status>=400&&this.parseFailure(n.status,n.data);let i=n.data||{};return Array.isArray(i.conflicts)?i.conflicts.map(g):[]}async resolveConflicts(t){var l;let e=await this.listConflicts({scope:t.scope}),n=new Map(e.map(p=>[p.conflictId,p])),i=t.resolutions.map(p=>n.get(p.conflictId)).filter(p=>!!p).map(p=>({table:p.entity,id:p.recordId})),c=await this.buildHeaders(t.scope),s={resolutions:i},o=await this.options.httpAdapter.request({method:"POST",url:this.normalizeUrl("/sync/conflicts/resolve"),headers:c,body:s});o.status>=400&&this.parseFailure(o.status,o.data);let a=Array.isArray((l=o.data)==null?void 0:l.remaining)?o.data.remaining.length:0;return{resolvedCount:Math.max(0,i.length-a)}}};export{d as DEFAULT_SYNC_POLICY,h as InMemoryCheckpointStore,y as InMemoryOutboxStore,_ as OFSYNC_CONTRACT_STAGE,u as OfsyncCore,S as OfsyncCoreBuilder,b as OfsyncHttpTransport,m as OfsyncTransportError,q as createSyncRuntime,f as defaultConflictResolver,G as registerDomainBridge,z as startSync,Y as stopSync};
|
|
1
|
+
var A={batchSize:200,maxRetry:5,scheduler:{mode:"manual",onlineTrigger:!0},retryBackoff:{strategy:"exponential-jitter",baseDelayMs:500,maxDelayMs:3e4}};var O=class{constructor(){this.rows=new Map}async enqueue(t){this.rows.set(t.outboxId,t)}async listPending(t){return[...this.rows.values()].filter(n=>n.status==="PENDING"||n.status==="FAILED").sort((n,r)=>n.createdAt.localeCompare(r.createdAt)).slice(0,Math.max(0,t))}async findByIdempotencyKey(t){for(let n of this.rows.values())if(n.idempotencyKey===t)return n;return null}async listAll(){return[...this.rows.values()].sort((t,n)=>t.createdAt.localeCompare(n.createdAt))}async update(t){this.rows.set(t.outboxId,t)}async remove(t){this.rows.delete(t)}};var k=class{constructor(){this.rows=new Map}async get(t){return this.rows.has(t)?this.rows.get(t):null}async set(t,n){this.rows.set(t,n)}};function _(e){return e.code==="VERSION_CONFLICT"?{conflictId:e.conflictId,action:"APPLY_REMOTE"}:e.code==="SCOPE_CONFLICT"?{conflictId:e.conflictId,action:"MANUAL_MERGE"}:{conflictId:e.conflictId,action:"KEEP_LOCAL"}}var tt="phase1-runtime-skeleton";import{CoreRuntime as M}from"ofcore";import{InMemoryDbAdapter as B}from"ofcore";import{asReadonlyStore as F,createStore as U}from"ofcore";function H(e={}){return{...A,...e,scheduler:{...A.scheduler,...e.scheduler||{}},retryBackoff:{...A.retryBackoff,...e.retryBackoff||{}}}}function $(e){return`${e.tenantId||"__standalone__"}::${e.branchId||"__all__"}::${e.ledgerProfileId||"__default_ledger__"}::${e.deviceId||"__device__"}`}function j(e,t){return`${$(e)}::${t}`}function K(e){if(typeof e!="string"||e.trim().length===0)return null;let t=Date.parse(e);return Number.isFinite(t)?new Date(t+5e3).toISOString():null}var I=class{constructor(t,n,r={}){this.runtime=t;this.runtimeStateStore=n;this.bridges=new Map;this.runtimeStore=F(this.runtimeStateStore),this.policy=H(r.policy),this.transport=r.transport,this.outboxStore=r.outboxStore||new O,this.checkpointStore=r.checkpointStore||new k,this.conflictResolvers=r.conflictResolvers||[],this.projectionHook=r.projectionHook}static builder(){return new x}get registry(){return this.runtime.registry}getPolicy(){return this.policy}registerDomainBridge(t){let n=t.domain.trim();if(!n)throw new Error("DomainBridge.domain is required");if(this.bridges.has(n))throw new Error(`DomainBridge for domain "${n}" is already registered`);this.bridges.set(n,t)}listDomainBridges(){return[...this.bridges.keys()].sort()}async getOutboxState(){let t=await this.outboxStore.listAll(),n=t.filter(i=>i.status==="PENDING").length,r=t.filter(i=>i.status==="FAILED").length;return{pending:n,failed:r,total:t.length}}async enqueueLocalChange(t,n){let r=(n||`${t.domain}:${t.entity}:${t.recordId}:${t.type}:${t.id}`).trim();if(!r)throw new Error("idempotencyKey must not be empty");let i=await this.outboxStore.findByIdempotencyKey(r);if(i)return i;let a=new Date().toISOString(),c={outboxId:`outbox_${Date.now()}_${Math.random().toString(36).slice(2)}`,idempotencyKey:r,change:t,retryCount:0,status:"PENDING",createdAt:a,updatedAt:a};return await this.outboxStore.enqueue(c),c}async start(t={}){this.runtimeStateStore.setState({phase:"starting",started:!1,syncing:!1,lastError:null,lastTransitionAt:new Date().toISOString()});try{await this.runtime.start(t),this.runtimeStateStore.setState(n=>({...n,phase:"started",started:!0,syncing:!1,startCount:n.startCount+1,lastError:null,lastTransitionAt:new Date().toISOString()}))}catch(n){throw this.runtimeStateStore.setState({phase:"error",started:!1,syncing:!1,lastError:n instanceof Error?n.message:String(n),lastTransitionAt:new Date().toISOString()}),n}}async stop(){this.stopScheduler(),this.runtimeStateStore.setState({phase:"stopping",started:this.runtime.isStarted(),syncing:!1,lastError:null,lastTransitionAt:new Date().toISOString()});try{await this.runtime.stop(),this.runtimeStateStore.setState(t=>({...t,phase:"stopped",started:!1,syncing:!1,stopCount:t.stopCount+1,lastError:null,lastTransitionAt:new Date().toISOString()}))}catch(t){throw this.runtimeStateStore.setState({phase:"error",started:this.runtime.isStarted(),syncing:!1,lastError:t instanceof Error?t.message:String(t),lastTransitionAt:new Date().toISOString()}),t}}async startSync(t){if(!this.runtime.isStarted())throw new Error("Cannot start sync before runtime is started");this.runtimeStateStore.setState({phase:"syncing",started:!0,syncing:!0,lastError:null,lastTransitionAt:new Date().toISOString()});try{if(this.transport){await this.processOutbox(t);let n=this.listDomainBridges();for(let r of n){let i=j(t,r),a=await this.checkpointStore.get(i),c;try{c=await this.transport.pull({scope:t,cursor:a,limit:this.policy.batchSize,domain:r})}catch(u){let o=(u==null?void 0:u.code)||"",s=u==null?void 0:u.details,p=K(s==null?void 0:s.cutoffTime);if(o!=="SYNC_RESYNC_REQUIRED"||!p)throw u;await this.checkpointStore.set(i,p),a=p,c=await this.transport.pull({scope:t,cursor:a,limit:this.policy.batchSize,domain:r})}let l=(c.changes||[]).map(u=>{let o=String(u.domain||"").trim().toLowerCase();if(!o)return{...u,domain:r};if(o!==r)throw new Error(`Sync pull domain mismatch: expected ${r}, received ${o}`);return u});await this.applyRemoteChanges(l,t),c.nextCursor!==void 0&&await this.checkpointStore.set(i,c.nextCursor)}}this.runtimeStateStore.setState(n=>({...n,phase:"started",started:!0,syncing:!1,syncCount:n.syncCount+1,lastError:null,lastTransitionAt:new Date().toISOString()}))}catch(n){throw this.runtimeStateStore.setState({phase:"error",started:this.runtime.isStarted(),syncing:!1,lastError:n instanceof Error?n.message:String(n),lastTransitionAt:new Date().toISOString()}),n}}async stopSync(){this.runtimeStateStore.setState(t=>({...t,phase:this.runtime.isStarted()?"started":"stopped",syncing:!1,lastTransitionAt:new Date().toISOString()}))}isStarted(){return this.runtime.isStarted()}startScheduler(t){if(this.policy.scheduler.mode!=="interval")return;let n=this.policy.scheduler.intervalMs||1e4;this.stopScheduler(),this.schedulerTimer=setInterval(()=>{this.startSync(t).catch(r=>{this.runtimeStateStore.setState(i=>({...i,phase:"error",syncing:!1,lastError:r instanceof Error?r.message:String(r),lastTransitionAt:new Date().toISOString()}))})},n)}stopScheduler(){this.schedulerTimer&&(clearInterval(this.schedulerTimer),this.schedulerTimer=void 0)}async notifyOnline(t){this.policy.scheduler.onlineTrigger!==!1&&await this.startSync(t)}async processOutbox(t){var i;if(!this.transport)return;let n=await this.outboxStore.listPending(this.policy.batchSize);if(n.length===0)return;let r=new Map;for(let a of n){let c=String(a.change.domain||"").trim().toLowerCase()||"__unknown__";r.has(c)||r.set(c,[]),(i=r.get(c))==null||i.push(a)}for(let[a,c]of r.entries()){if(a==="__unknown__"){for(let p of c){let d=p.retryCount+1;await this.outboxStore.update({...p,retryCount:d,status:d>=this.policy.maxRetry?"FAILED":"PENDING",lastError:"DOMAIN_REQUIRED",updatedAt:new Date().toISOString()})}continue}let l=await this.transport.push({scope:t,outbox:c,domain:a}),u=new Set((l.applied||[]).map(p=>p.changeId)),o=new Map((l.failed||[]).map(p=>[p.changeId,p])),s=new Set((l.conflicts||[]).map(p=>p.changeId));await this.resolveConflicts(l.conflicts||[],t);for(let p of c){if(u.has(p.change.id)){await this.outboxStore.remove(p.outboxId);continue}let d=o.get(p.change.id);if(d||s.has(p.change.id)){let g=p.retryCount+1;await this.outboxStore.update({...p,retryCount:g,status:g>=this.policy.maxRetry?"FAILED":"PENDING",lastError:(d==null?void 0:d.message)||(s.has(p.change.id)?"SYNC_CONFLICT":p.lastError),updatedAt:new Date().toISOString()})}}}}async applyRemoteChanges(t,n){if(!Array.isArray(t)||t.length===0)return;let r=[...t].sort((a,c)=>a.occurredAt.localeCompare(c.occurredAt)),i=new Map;for(let a of r){i.has(a.domain)||i.set(a.domain,[]);let c=i.get(a.domain);c&&c.push(a)}for(let[a,c]of i.entries()){let l=this.bridges.get(a);if(l!=null&&l.applyRemoteChanges&&(await l.applyRemoteChanges(c,n),this.projectionHook))for(let u of c)await this.projectionHook(u,n)}}async resolveConflicts(t,n){var i;if(!((i=this.transport)!=null&&i.resolveConflicts)||t.length===0)return;let r=[];for(let a of t){let c=[...this.conflictResolvers],l=null;for(let u of c){let o=await u(a,{scope:n});if(o){l=o;break}}l||(l=_(a)),r.push(l)}r.length>0&&await this.transport.resolveConflicts({scope:n,resolutions:r})}},x=class{constructor(){this.runtimeBuilder=M.builder();this.options={};this.runtimeBuilder.withDbAdapter(()=>new B)}withPlatformAdapter(t){return this.runtimeBuilder.withPlatformAdapter(t),this}withDbAdapter(t){return this.runtimeBuilder.withDbAdapter(t),this}withHttpAdapter(t){return this.runtimeBuilder.withHttpAdapter(t),this}withSocketAdapter(t){return this.runtimeBuilder.withSocketAdapter(t),this}withLoggerAdapter(t){return this.runtimeBuilder.withLoggerAdapter(t),this}withActivitySink(t){return this.runtimeBuilder.withActivitySink(t),this}withExtension(t,n){return this.runtimeBuilder.withExtension(t,n),this}withPolicy(t){return this.options={...this.options,policy:t},this}withTransport(t){return this.options={...this.options,transport:t},this}withOutboxStore(t){return this.options={...this.options,outboxStore:t},this}withCheckpointStore(t){return this.options={...this.options,checkpointStore:t},this}withConflictResolvers(t){return this.options={...this.options,conflictResolvers:t},this}withProjectionHook(t){return this.options={...this.options,projectionHook:t},this}build(){let t=U({phase:"idle",started:!1,syncing:!1,startCount:0,stopCount:0,syncCount:0,lastError:null,lastTransitionAt:new Date().toISOString()}),n=this.runtimeBuilder.build();return new I(n,t,this.options)}};function dt(e={}){let t=I.builder();return e.policy&&t.withPolicy(e.policy),e.transport&&t.withTransport(e.transport),e.outboxStore&&t.withOutboxStore(e.outboxStore),e.checkpointStore&&t.withCheckpointStore(e.checkpointStore),e.conflictResolvers&&t.withConflictResolvers(e.conflictResolvers),e.projectionHook&&t.withProjectionHook(e.projectionHook),t.build()}function pt(e,t){e.registerDomainBridge(t)}async function ht(e,t,n={}){e.isStarted()||await e.start(n),await e.startSync(t)}async function yt(e){await e.stopSync()}var w=class extends Error{constructor(t,n,r=!1,i,a){super(n),this.name="OfsyncTransportError",this.code=t,this.retryable=r,this.status=i,this.details=a}};async function z(e){let t=globalThis.crypto;if(!t||!t.subtle)throw new Error("WebCrypto API is not available");let n=new TextEncoder().encode(e),r=await t.subtle.digest("SHA-256",n);return[...new Uint8Array(r)].map(i=>i.toString(16).padStart(2,"0")).join("")}function Y(e){let t=String(e.table||""),n=String(e.id||""),r=String(e.changeId||"");return[t,n,r].join("::")}function D(e){return{conflictId:Y(e),changeId:String(e.changeId||""),domain:"unknown",entity:String(e.table||""),recordId:String(e.id||""),code:String(e.code||"UNKNOWN_CONFLICT"),message:String(e.message||"Conflict detected"),retryable:!!e.retryable,scope:{tenantId:typeof e.tenantId=="string"?e.tenantId:void 0,branchId:typeof e.branchId=="string"?e.branchId:void 0,ledgerProfileId:typeof e.ledgerProfileId=="string"?e.ledgerProfileId:typeof e.financeDomainId=="string"?e.financeDomainId:void 0},details:e}}function R(e){return!!(e&&typeof e=="object"&&!Array.isArray(e))}function G(e){var n;let t=(n=e.branchId)==null?void 0:n.trim();if(!t)throw new w("BRANCH_SCOPE_REQUIRED","Sync scope requires branchId",!1);return t}var L=class{constructor(t){this.options=t}nowIso(){return this.options.nowIso?this.options.nowIso():new Date().toISOString()}defaultBootstrapSinceIso(){return new Date(Date.now()-2160*60*60*1e3).toISOString()}async buildHeaders(t){let n={"content-type":"application/json","x-branch-id":G(t),"x-sync-protocol-version":"1"};if(t.tenantId&&(n["x-tenant-id"]=t.tenantId),t.deviceId&&(n["x-device-id"]=t.deviceId),t.ledgerProfileId&&(n["x-ledger-profile-id"]=t.ledgerProfileId),this.options.getAccessToken){let r=await this.options.getAccessToken();r&&(n.authorization=`Bearer ${r}`)}return n}normalizeUrl(t){return`${this.options.baseUrl.replace(/\/+$/,"")}${t}`}normalizeDomainPath(t){let n=t.trim().toLowerCase().replace(/[^a-z0-9_-]/g,"");return n||null}resolveDomainPathOrThrow(t){let n=this.normalizeDomainPath(t||"");if(!n)throw new w("DOMAIN_REQUIRED","Sync transport requires explicit domain",!1);return n}isGenericFallbackEnabled(){return this.options.allowGenericFallback!==!1}async requestWithDomainPolicy(t,n,r){if(n){let i=await this.options.httpAdapter.request({...t,url:this.normalizeUrl(n)});if(i.status!==404||!this.isGenericFallbackEnabled())return i}return this.options.httpAdapter.request({...t,url:this.normalizeUrl(r)})}parseFailure(t,n){var u,o,s;let r=n||{},i=((u=r.error)==null?void 0:u.code)||"SYNC_TRANSPORT_ERROR",a=((o=r.error)==null?void 0:o.message)||`Sync transport failed with status ${t}`,c=!!((s=r.error)!=null&&s.retryable),l=R(r.error)&&R(r.error.details)?r.error.details:void 0;throw new w(i,a,c,t,l)}async push(t){let n={changes:t.outbox.map(o=>{let s=R(o.change.payload)?o.change.payload:{},p=typeof s.table=="string"?s.table.trim():"",d=R(s.record)?s.record:null,g=Date.parse(o.change.occurredAt);return p&&d?{id:o.change.id,table:p,type:o.change.type,record:{...d,id:o.change.recordId||String(d.id||o.change.id)},updated_at:Number.isFinite(g)?g:o.change.occurredAt}:{id:o.change.id,entity:o.change.entity,type:o.change.type,data:{id:o.change.recordId,...s},timestamp:o.change.occurredAt}})},r=JSON.stringify(n),i=await z(r),a=await this.buildHeaders(t.scope);a["x-data-checksum"]=i;let c=this.resolveDomainPathOrThrow(t.domain),l=await this.requestWithDomainPolicy({method:"POST",headers:a,body:n},`/sync/${c}/push`,"/sync/push");l.status>=400&&this.parseFailure(l.status,l.data);let u=l.data||{};return{applied:Array.isArray(u.applied)?u.applied:[],failed:Array.isArray(u.failed)?u.failed:[],conflicts:Array.isArray(u.conflicts)?u.conflicts.map(D):[]}}async pull(t){let n=await this.buildHeaders(t.scope),r=t.cursor||this.defaultBootstrapSinceIso(),i=this.resolveDomainPathOrThrow(t.domain),a=await this.requestWithDomainPolicy({method:"GET",headers:n,query:{since:r}},`/sync/${i}/pull`,"/sync/pull");a.status>=400&&this.parseFailure(a.status,a.data);let c=a.data||{},l=Array.isArray(c.changes)?c.changes.map(o=>{var d,g,f,h;let s=typeof o.table=="string"?o.table:"",p=typeof o.domain=="string"&&o.domain.trim().length>0?o.domain:s.startsWith("auth_")?"ofauth":s.startsWith("offinance_")?"offinance":s.startsWith("ofcoop_")?"ofcoop":s.startsWith("ofpos_")?"ofpos":"unknown";return{id:String(o.changeId||o.id||""),domain:String(p),entity:String(o.entity||s||""),type:String(o.type||o.action||"UPDATE"),recordId:String(o.recordId||((d=o.record)==null?void 0:d.id)||((g=o.data)==null?void 0:g.id)||o.id||""),payload:typeof s=="string"&&s.length>0?{table:String(s),action:String(o.action||o.type||"UPDATE"),record:{...o.record||o.data||{},id:String(o.recordId||((f=o.record)==null?void 0:f.id)||((h=o.data)==null?void 0:h.id)||o.id||"")}}:o.record||o.data||{},occurredAt:String(o.timestamp||this.nowIso()),scope:t.scope}}):[],u=l.length>0?l[l.length-1].occurredAt:this.nowIso();return{changes:l,nextCursor:u,conflicts:[]}}async listConflicts(t){let n=await this.buildHeaders(t.scope),r=await this.options.httpAdapter.request({method:"GET",url:this.normalizeUrl("/sync/conflicts"),headers:n});r.status>=400&&this.parseFailure(r.status,r.data);let i=r.data||{};return Array.isArray(i.conflicts)?i.conflicts.map(D):[]}async resolveConflicts(t){var o;let n=await this.listConflicts({scope:t.scope}),r=new Map(n.map(s=>[s.conflictId,s])),i=t.resolutions.map(s=>r.get(s.conflictId)).filter(s=>!!s).map(s=>({table:s.entity,id:s.recordId})),a=await this.buildHeaders(t.scope),c={resolutions:i},l=await this.options.httpAdapter.request({method:"POST",url:this.normalizeUrl("/sync/conflicts/resolve"),headers:a,body:c});l.status>=400&&this.parseFailure(l.status,l.data);let u=Array.isArray((o=l.data)==null?void 0:o.remaining)?l.data.remaining.length:0;return{resolvedCount:Math.max(0,i.length-u)}}};function N(e){return`Tidak dapat terhubung ke server sinkronisasi (${e.trim()||"VITE_OFSYNC_BASE_URL"}). Pastikan ofsync-server berjalan dan CORS origin browser diizinkan.`}function q(e){return e.trim().replace(/\/+$/,"")}function W(e){return e instanceof Error?e.message:String(e!=null?e:"")}function X(e,t=9e5){if(typeof e=="number"&&Number.isFinite(e)&&e>0)return Math.floor(e*1e3);if(typeof e!="string")return t;let n=e.trim();if(!n)return t;if(/^\d+$/.test(n))return Math.max(1,Number(n))*1e3;let r=n.match(/^(\d+)\s*([smhd])$/i);if(!r)return t;let i=Number(r[1]),a=r[2].toLowerCase(),c=a==="s"?1e3:a==="m"?6e4:a==="h"?36e5:864e5;return Math.max(1,i)*c}function v(e,t,n=N){let r=W(e),i=r.toLowerCase();return i.includes("networkerror")||i.includes("failed to fetch")||i.includes("load failed")||i.includes("fetch failed")||i.includes("the network connection was lost")||i.includes("network request failed")?n(t):r}function C(e,t){let n=new Error(t);return n.code=e,n}async function mt(e){var i,a,c,l;if(e.bearerToken.length>0)return e.bearerToken;let t=(i=e.nowMs)!=null?i:Date.now(),n=(a=e.refreshLeewayMs)!=null?a:1e4;if((c=e.session)!=null&&c.accessToken){if(e.session.expiresAtMs>t+n)return e.session.accessToken;let u=await e.refresh();if(u!=null&&u.accessToken)return u.accessToken}let r=await e.loginFromCredentials();return(l=r==null?void 0:r.accessToken)!=null?l:""}function St(e){var l,u,o;let t=(l=e.fetchImpl)!=null?l:fetch,n=q(e.baseUrl),r=(u=e.accessTokenFallbackTtlMs)!=null?u:9e5,i=(o=e.networkErrorMessage)!=null?o:N,a=s=>(Array.isArray(s.scopes)?s.scopes:[]).filter(d=>!!(d&&typeof d=="object")).map(d=>{var g,f,h,y;return{tenantId:typeof d.tenantId=="string"&&d.tenantId.trim().length>0?d.tenantId:null,branchId:String((g=d.branchId)!=null?g:"").trim(),role:String((f=d.role)!=null?f:""),userId:String((h=d.userId)!=null?h:""),email:String((y=d.email)!=null?y:"")}}).filter(d=>d.branchId.length>0),c=(s,p)=>{var f,h,y;let d=String((h=(f=s.accessToken)!=null?f:s.token)!=null?h:""),g=String((y=s.refreshToken)!=null?y:"");if(!d||!g)throw C("SYNC_AUTH_TOKEN_INCOMPLETE","Sync auth gagal: token tidak lengkap.");return{accessToken:d,refreshToken:g,expiresAtMs:Date.now()+X(s.expiresIn,r),principal:p}};return{async fetchScopeOptions(s,p){var d,g,f;if(!n)return[];try{let h=await t(`${n}/api/v1/auth/scope-options`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:s,password:p})}),y=await h.json().catch(()=>({}));if(!h.ok){let m=(d=y.error)!=null?d:{};throw C(String((g=m.code)!=null?g:`HTTP_${h.status}`),String((f=m.message)!=null?f:`Sync scope discovery gagal (${h.status})`))}return a(y)}catch(h){let y=h;throw C(typeof(y==null?void 0:y.code)=="string"?y.code:"SYNC_AUTH_SCOPE_DISCOVERY_FAILED",v(h,n,i))}},async login(s,p,d){var h,y,m,E,T;let g=String((h=d.branchId)!=null?h:"").trim(),f=String((y=d.tenantId)!=null?y:"").trim();try{let S=await t(`${n}/api/v1/auth/login`,{method:"POST",headers:{"Content-Type":"application/json","X-Branch-ID":g,...f?{"X-Tenant-ID":f}:{},"X-Device-ID":e.deviceId},body:JSON.stringify({username:s,password:p})}),b=await S.json().catch(()=>({}));if(!S.ok){let P=(m=b.error)!=null?m:{};throw C(String((E=P.code)!=null?E:`HTTP_${S.status}`),String((T=P.message)!=null?T:`Sync auth login gagal (${S.status})`))}return c(b,s)}catch(S){let b=S;throw C(typeof(b==null?void 0:b.code)=="string"?b.code:"SYNC_AUTH_FAILED",v(S,n,i))}},async refresh(s,p){var d,g,f;try{let h=await t(`${n}/api/v1/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json",...p?{"X-Tenant-ID":p}:{},"X-Device-ID":e.deviceId},body:JSON.stringify({refreshToken:s})}),y=await h.json().catch(()=>({}));if(!h.ok){let m=(d=y.error)!=null?d:{};throw C(String((g=m.code)!=null?g:`HTTP_${h.status}`),String((f=m.message)!=null?f:`Sync auth refresh gagal (${h.status})`))}return c(y,"")}catch(h){let y=h;throw C(typeof(y==null?void 0:y.code)=="string"?y.code:"SYNC_AUTH_REFRESH_FAILED",v(h,n,i))}}}}export{A as DEFAULT_SYNC_POLICY,k as InMemoryCheckpointStore,O as InMemoryOutboxStore,tt as OFSYNC_CONTRACT_STAGE,I as OfsyncCore,x as OfsyncCoreBuilder,L as OfsyncHttpTransport,w as OfsyncTransportError,St as createOfsyncAuthClient,C as createSyncAuthError,dt as createSyncRuntime,_ as defaultConflictResolver,v as normalizeSyncNetworkError,X as parseSyncDurationMs,pt as registerDomainBridge,mt as resolveSyncAccessToken,ht as startSync,yt as stopSync};
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var g=Object.defineProperty;var x=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var R=Object.prototype.hasOwnProperty;var O=(r,t)=>{for(var e in t)g(r,e,{get:t[e],enumerable:!0})},P=(r,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of A(t))!R.call(r,o)&&o!==e&&g(r,o,{get:()=>t[o],enumerable:!(n=x(t,o))||n.enumerable});return r};var E=r=>P(g({},"__esModule",{value:!0}),r);var U={};O(U,{DEFAULT_SYNC_POLICY:()=>d,InMemoryCheckpointStore:()=>h,InMemoryOutboxStore:()=>y,OFSYNC_CONTRACT_STAGE:()=>k,OfsyncCore:()=>u,OfsyncCoreBuilder:()=>S,OfsyncHttpTransport:()=>C,OfsyncTransportError:()=>m,createSyncRuntime:()=>_,defaultConflictResolver:()=>b,registerDomainBridge:()=>N,startSync:()=>B,stopSync:()=>L});module.exports=E(U);var d={batchSize:200,maxRetry:5,scheduler:{mode:"manual",onlineTrigger:!0},retryBackoff:{strategy:"exponential-jitter",baseDelayMs:500,maxDelayMs:3e4}};var y=class{constructor(){this.rows=new Map}async enqueue(t){this.rows.set(t.outboxId,t)}async listPending(t){return[...this.rows.values()].filter(e=>e.status==="PENDING"||e.status==="FAILED").sort((e,n)=>e.createdAt.localeCompare(n.createdAt)).slice(0,Math.max(0,t))}async findByIdempotencyKey(t){for(let e of this.rows.values())if(e.idempotencyKey===t)return e;return null}async update(t){this.rows.set(t.outboxId,t)}async remove(t){this.rows.delete(t)}};var h=class{constructor(){this.rows=new Map}async get(t){return this.rows.has(t)?this.rows.get(t):null}async set(t,e){this.rows.set(t,e)}};function b(r){return r.code==="VERSION_CONFLICT"?{conflictId:r.conflictId,action:"APPLY_REMOTE"}:r.code==="SCOPE_CONFLICT"?{conflictId:r.conflictId,action:"MANUAL_MERGE"}:{conflictId:r.conflictId,action:"KEEP_LOCAL"}}var k="phase1-runtime-skeleton";var I=require("ofcore"),w=require("ofcore"),f=require("ofcore");function T(r={}){return{...d,...r,scheduler:{...d.scheduler,...r.scheduler||{}},retryBackoff:{...d.retryBackoff,...r.retryBackoff||{}}}}function D(r){return`${r.tenantId||"__standalone__"}::${r.branchId||"__all__"}::${r.deviceId||"__device__"}`}var u=class{constructor(t,e,n={}){this.runtime=t;this.runtimeStateStore=e;this.bridges=new Map;this.runtimeStore=(0,f.asReadonlyStore)(this.runtimeStateStore),this.policy=T(n.policy),this.transport=n.transport,this.outboxStore=n.outboxStore||new y,this.checkpointStore=n.checkpointStore||new h,this.conflictResolvers=n.conflictResolvers||[],this.projectionHook=n.projectionHook}static builder(){return new S}get registry(){return this.runtime.registry}getPolicy(){return this.policy}registerDomainBridge(t){let e=t.domain.trim();if(!e)throw new Error("DomainBridge.domain is required");if(this.bridges.has(e))throw new Error(`DomainBridge for domain "${e}" is already registered`);this.bridges.set(e,t)}listDomainBridges(){return[...this.bridges.keys()].sort()}async enqueueLocalChange(t,e){let n=(e||`${t.domain}:${t.entity}:${t.recordId}:${t.type}:${t.id}`).trim();if(!n)throw new Error("idempotencyKey must not be empty");let o=await this.outboxStore.findByIdempotencyKey(n);if(o)return o;let c=new Date().toISOString(),s={outboxId:`outbox_${Date.now()}_${Math.random().toString(36).slice(2)}`,idempotencyKey:n,change:t,retryCount:0,status:"PENDING",createdAt:c,updatedAt:c};return await this.outboxStore.enqueue(s),s}async start(t={}){this.runtimeStateStore.setState({phase:"starting",started:!1,syncing:!1,lastError:null,lastTransitionAt:new Date().toISOString()});try{await this.runtime.start(t),this.runtimeStateStore.setState(e=>({...e,phase:"started",started:!0,syncing:!1,startCount:e.startCount+1,lastError:null,lastTransitionAt:new Date().toISOString()}))}catch(e){throw this.runtimeStateStore.setState({phase:"error",started:!1,syncing:!1,lastError:e instanceof Error?e.message:String(e),lastTransitionAt:new Date().toISOString()}),e}}async stop(){this.stopScheduler(),this.runtimeStateStore.setState({phase:"stopping",started:this.runtime.isStarted(),syncing:!1,lastError:null,lastTransitionAt:new Date().toISOString()});try{await this.runtime.stop(),this.runtimeStateStore.setState(t=>({...t,phase:"stopped",started:!1,syncing:!1,stopCount:t.stopCount+1,lastError:null,lastTransitionAt:new Date().toISOString()}))}catch(t){throw this.runtimeStateStore.setState({phase:"error",started:this.runtime.isStarted(),syncing:!1,lastError:t instanceof Error?t.message:String(t),lastTransitionAt:new Date().toISOString()}),t}}async startSync(t){if(!this.runtime.isStarted())throw new Error("Cannot start sync before runtime is started");this.runtimeStateStore.setState({phase:"syncing",started:!0,syncing:!0,lastError:null,lastTransitionAt:new Date().toISOString()});try{let e=D(t),n=await this.checkpointStore.get(e);if(this.transport){await this.processOutbox(t);let o=await this.transport.pull({scope:t,cursor:n,limit:this.policy.batchSize});await this.applyRemoteChanges(o.changes||[],t),o.nextCursor!==void 0&&await this.checkpointStore.set(e,o.nextCursor)}this.runtimeStateStore.setState(o=>({...o,phase:"started",started:!0,syncing:!1,syncCount:o.syncCount+1,lastError:null,lastTransitionAt:new Date().toISOString()}))}catch(e){throw this.runtimeStateStore.setState({phase:"error",started:this.runtime.isStarted(),syncing:!1,lastError:e instanceof Error?e.message:String(e),lastTransitionAt:new Date().toISOString()}),e}}async stopSync(){this.runtimeStateStore.setState(t=>({...t,phase:this.runtime.isStarted()?"started":"stopped",syncing:!1,lastTransitionAt:new Date().toISOString()}))}isStarted(){return this.runtime.isStarted()}startScheduler(t){if(this.policy.scheduler.mode!=="interval")return;let e=this.policy.scheduler.intervalMs||1e4;this.stopScheduler(),this.schedulerTimer=setInterval(()=>{this.startSync(t).catch(n=>{this.runtimeStateStore.setState(o=>({...o,phase:"error",syncing:!1,lastError:n instanceof Error?n.message:String(n),lastTransitionAt:new Date().toISOString()}))})},e)}stopScheduler(){this.schedulerTimer&&(clearInterval(this.schedulerTimer),this.schedulerTimer=void 0)}async notifyOnline(t){this.policy.scheduler.onlineTrigger!==!1&&await this.startSync(t)}async processOutbox(t){if(!this.transport)return;let e=await this.outboxStore.listPending(this.policy.batchSize);if(e.length===0)return;let n=await this.transport.push({scope:t,outbox:e}),o=new Set((n.applied||[]).map(i=>i.changeId)),c=new Map((n.failed||[]).map(i=>[i.changeId,i])),s=new Set((n.conflicts||[]).map(i=>i.changeId));await this.resolveConflicts(n.conflicts||[],t);for(let i of e){if(o.has(i.change.id)){await this.outboxStore.remove(i.outboxId);continue}let a=c.get(i.change.id);if(a||s.has(i.change.id)){let l=i.retryCount+1;await this.outboxStore.update({...i,retryCount:l,status:l>=this.policy.maxRetry?"FAILED":"PENDING",lastError:(a==null?void 0:a.message)||(s.has(i.change.id)?"SYNC_CONFLICT":i.lastError),updatedAt:new Date().toISOString()})}}}async applyRemoteChanges(t,e){if(!Array.isArray(t)||t.length===0)return;let n=[...t].sort((c,s)=>c.occurredAt.localeCompare(s.occurredAt)),o=new Map;for(let c of n){o.has(c.domain)||o.set(c.domain,[]);let s=o.get(c.domain);s&&s.push(c)}for(let[c,s]of o.entries()){let i=this.bridges.get(c);if(i!=null&&i.applyRemoteChanges&&(await i.applyRemoteChanges(s,e),this.projectionHook))for(let a of s)await this.projectionHook(a,e)}}async resolveConflicts(t,e){var o;if(!((o=this.transport)!=null&&o.resolveConflicts)||t.length===0)return;let n=[];for(let c of t){let s=[...this.conflictResolvers],i=null;for(let a of s){let l=await a(c,{scope:e});if(l){i=l;break}}i||(i=b(c)),n.push(i)}n.length>0&&await this.transport.resolveConflicts({scope:e,resolutions:n})}},S=class{constructor(){this.runtimeBuilder=I.CoreRuntime.builder();this.options={};this.runtimeBuilder.withDbAdapter(()=>new w.InMemoryDbAdapter)}withPlatformAdapter(t){return this.runtimeBuilder.withPlatformAdapter(t),this}withDbAdapter(t){return this.runtimeBuilder.withDbAdapter(t),this}withHttpAdapter(t){return this.runtimeBuilder.withHttpAdapter(t),this}withSocketAdapter(t){return this.runtimeBuilder.withSocketAdapter(t),this}withLoggerAdapter(t){return this.runtimeBuilder.withLoggerAdapter(t),this}withActivitySink(t){return this.runtimeBuilder.withActivitySink(t),this}withExtension(t,e){return this.runtimeBuilder.withExtension(t,e),this}withPolicy(t){return this.options={...this.options,policy:t},this}withTransport(t){return this.options={...this.options,transport:t},this}withOutboxStore(t){return this.options={...this.options,outboxStore:t},this}withCheckpointStore(t){return this.options={...this.options,checkpointStore:t},this}withConflictResolvers(t){return this.options={...this.options,conflictResolvers:t},this}withProjectionHook(t){return this.options={...this.options,projectionHook:t},this}build(){let t=(0,f.createStore)({phase:"idle",started:!1,syncing:!1,startCount:0,stopCount:0,syncCount:0,lastError:null,lastTransitionAt:new Date().toISOString()}),e=this.runtimeBuilder.build();return new u(e,t,this.options)}};function _(r={}){let t=u.builder();return r.policy&&t.withPolicy(r.policy),r.transport&&t.withTransport(r.transport),r.outboxStore&&t.withOutboxStore(r.outboxStore),r.checkpointStore&&t.withCheckpointStore(r.checkpointStore),r.conflictResolvers&&t.withConflictResolvers(r.conflictResolvers),r.projectionHook&&t.withProjectionHook(r.projectionHook),t.build()}function N(r,t){r.registerDomainBridge(t)}async function B(r,t,e={}){r.isStarted()||await r.start(e),await r.startSync(t)}async function L(r){await r.stopSync()}var m=class extends Error{constructor(t,e,n=!1,o){super(e),this.name="OfsyncTransportError",this.code=t,this.retryable=n,this.status=o}};async function M(r){let t=globalThis.crypto;if(!t||!t.subtle)throw new Error("WebCrypto API is not available");let e=new TextEncoder().encode(r),n=await t.subtle.digest("SHA-256",e);return[...new Uint8Array(n)].map(o=>o.toString(16).padStart(2,"0")).join("")}function H(r){let t=String(r.table||""),e=String(r.id||""),n=String(r.changeId||"");return[t,e,n].join("::")}function v(r){return{conflictId:H(r),changeId:String(r.changeId||""),domain:"unknown",entity:String(r.table||""),recordId:String(r.id||""),code:String(r.code||"UNKNOWN_CONFLICT"),message:String(r.message||"Conflict detected"),retryable:!!r.retryable,scope:{tenantId:typeof r.tenantId=="string"?r.tenantId:void 0,branchId:typeof r.branchId=="string"?r.branchId:void 0},details:r}}function F(r){var e;let t=(e=r.branchId)==null?void 0:e.trim();if(!t)throw new m("BRANCH_SCOPE_REQUIRED","Sync scope requires branchId",!1);return t}var C=class{constructor(t){this.options=t}nowIso(){return this.options.nowIso?this.options.nowIso():new Date().toISOString()}async buildHeaders(t){let e={"content-type":"application/json","x-branch-id":F(t)};if(t.tenantId&&(e["x-tenant-id"]=t.tenantId),this.options.getAccessToken){let n=await this.options.getAccessToken();n&&(e.authorization=`Bearer ${n}`)}return e}normalizeUrl(t){return`${this.options.baseUrl.replace(/\/+$/,"")}${t}`}parseFailure(t,e){var i,a,l;let n=e||{},o=((i=n.error)==null?void 0:i.code)||"SYNC_TRANSPORT_ERROR",c=((a=n.error)==null?void 0:a.message)||`Sync transport failed with status ${t}`,s=!!((l=n.error)!=null&&l.retryable);throw new m(o,c,s,t)}async push(t){let e={changes:t.outbox.map(a=>({id:a.change.id,entity:a.change.entity,type:a.change.type,data:{id:a.change.recordId,...a.change.payload},timestamp:a.change.occurredAt}))},n=JSON.stringify(e),o=await M(n),c=await this.buildHeaders(t.scope);c["x-data-checksum"]=o;let s=await this.options.httpAdapter.request({method:"POST",url:this.normalizeUrl("/sync/push"),headers:c,body:e});s.status>=400&&this.parseFailure(s.status,s.data);let i=s.data||{};return{applied:Array.isArray(i.applied)?i.applied:[],failed:Array.isArray(i.failed)?i.failed:[],conflicts:Array.isArray(i.conflicts)?i.conflicts.map(v):[]}}async pull(t){let e=await this.buildHeaders(t.scope),n=t.cursor||this.nowIso(),o=await this.options.httpAdapter.request({method:"GET",url:this.normalizeUrl("/sync/pull"),headers:e,query:{since:n}});o.status>=400&&this.parseFailure(o.status,o.data);let c=o.data||{},s=Array.isArray(c.changes)?c.changes.map(a=>{var l;return{id:String(a.id||""),domain:String(a.domain||"unknown"),entity:String(a.entity||""),type:String(a.type||"UPDATE"),recordId:String(a.recordId||((l=a.record)==null?void 0:l.id)||""),payload:a.record||{},occurredAt:String(a.timestamp||this.nowIso()),scope:t.scope}}):[],i=s.length>0?s[s.length-1].occurredAt:n;return{changes:s,nextCursor:i,conflicts:[]}}async listConflicts(t){let e=await this.buildHeaders(t.scope),n=await this.options.httpAdapter.request({method:"GET",url:this.normalizeUrl("/sync/conflicts"),headers:e});n.status>=400&&this.parseFailure(n.status,n.data);let o=n.data||{};return Array.isArray(o.conflicts)?o.conflicts.map(v):[]}async resolveConflicts(t){var l;let e=await this.listConflicts({scope:t.scope}),n=new Map(e.map(p=>[p.conflictId,p])),o=t.resolutions.map(p=>n.get(p.conflictId)).filter(p=>!!p).map(p=>({table:p.entity,id:p.recordId})),c=await this.buildHeaders(t.scope),s={resolutions:o},i=await this.options.httpAdapter.request({method:"POST",url:this.normalizeUrl("/sync/conflicts/resolve"),headers:c,body:s});i.status>=400&&this.parseFailure(i.status,i.data);let a=Array.isArray((l=i.data)==null?void 0:l.remaining)?i.data.remaining.length:0;return{resolvedCount:Math.max(0,o.length-a)}}};
|
|
1
|
+
"use strict";var T=Object.defineProperty;var $=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var K=Object.prototype.hasOwnProperty;var z=(e,t)=>{for(var n in t)T(e,n,{get:t[n],enumerable:!0})},Y=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of j(t))!K.call(e,o)&&o!==n&&T(e,o,{get:()=>t[o],enumerable:!(r=$(t,o))||r.enumerable});return e};var G=e=>Y(T({},"__esModule",{value:!0}),e);var lt={};z(lt,{DEFAULT_SYNC_POLICY:()=>A,InMemoryCheckpointStore:()=>k,InMemoryOutboxStore:()=>O,OFSYNC_CONTRACT_STAGE:()=>q,OfsyncCore:()=>I,OfsyncCoreBuilder:()=>R,OfsyncHttpTransport:()=>_,OfsyncTransportError:()=>w,createOfsyncAuthClient:()=>ct,createSyncAuthError:()=>S,createSyncRuntime:()=>V,defaultConflictResolver:()=>P,normalizeSyncNetworkError:()=>E,parseSyncDurationMs:()=>H,registerDomainBridge:()=>Z,resolveSyncAccessToken:()=>at,startSync:()=>tt,stopSync:()=>et});module.exports=G(lt);var A={batchSize:200,maxRetry:5,scheduler:{mode:"manual",onlineTrigger:!0},retryBackoff:{strategy:"exponential-jitter",baseDelayMs:500,maxDelayMs:3e4}};var O=class{constructor(){this.rows=new Map}async enqueue(t){this.rows.set(t.outboxId,t)}async listPending(t){return[...this.rows.values()].filter(n=>n.status==="PENDING"||n.status==="FAILED").sort((n,r)=>n.createdAt.localeCompare(r.createdAt)).slice(0,Math.max(0,t))}async findByIdempotencyKey(t){for(let n of this.rows.values())if(n.idempotencyKey===t)return n;return null}async listAll(){return[...this.rows.values()].sort((t,n)=>t.createdAt.localeCompare(n.createdAt))}async update(t){this.rows.set(t.outboxId,t)}async remove(t){this.rows.delete(t)}};var k=class{constructor(){this.rows=new Map}async get(t){return this.rows.has(t)?this.rows.get(t):null}async set(t,n){this.rows.set(t,n)}};function P(e){return e.code==="VERSION_CONFLICT"?{conflictId:e.conflictId,action:"APPLY_REMOTE"}:e.code==="SCOPE_CONFLICT"?{conflictId:e.conflictId,action:"MANUAL_MERGE"}:{conflictId:e.conflictId,action:"KEEP_LOCAL"}}var q="phase1-runtime-skeleton";var M=require("ofcore"),B=require("ofcore"),x=require("ofcore");function W(e={}){return{...A,...e,scheduler:{...A.scheduler,...e.scheduler||{}},retryBackoff:{...A.retryBackoff,...e.retryBackoff||{}}}}function X(e){return`${e.tenantId||"__standalone__"}::${e.branchId||"__all__"}::${e.ledgerProfileId||"__default_ledger__"}::${e.deviceId||"__device__"}`}function J(e,t){return`${X(e)}::${t}`}function Q(e){if(typeof e!="string"||e.trim().length===0)return null;let t=Date.parse(e);return Number.isFinite(t)?new Date(t+5e3).toISOString():null}var I=class{constructor(t,n,r={}){this.runtime=t;this.runtimeStateStore=n;this.bridges=new Map;this.runtimeStore=(0,x.asReadonlyStore)(this.runtimeStateStore),this.policy=W(r.policy),this.transport=r.transport,this.outboxStore=r.outboxStore||new O,this.checkpointStore=r.checkpointStore||new k,this.conflictResolvers=r.conflictResolvers||[],this.projectionHook=r.projectionHook}static builder(){return new R}get registry(){return this.runtime.registry}getPolicy(){return this.policy}registerDomainBridge(t){let n=t.domain.trim();if(!n)throw new Error("DomainBridge.domain is required");if(this.bridges.has(n))throw new Error(`DomainBridge for domain "${n}" is already registered`);this.bridges.set(n,t)}listDomainBridges(){return[...this.bridges.keys()].sort()}async getOutboxState(){let t=await this.outboxStore.listAll(),n=t.filter(o=>o.status==="PENDING").length,r=t.filter(o=>o.status==="FAILED").length;return{pending:n,failed:r,total:t.length}}async enqueueLocalChange(t,n){let r=(n||`${t.domain}:${t.entity}:${t.recordId}:${t.type}:${t.id}`).trim();if(!r)throw new Error("idempotencyKey must not be empty");let o=await this.outboxStore.findByIdempotencyKey(r);if(o)return o;let a=new Date().toISOString(),c={outboxId:`outbox_${Date.now()}_${Math.random().toString(36).slice(2)}`,idempotencyKey:r,change:t,retryCount:0,status:"PENDING",createdAt:a,updatedAt:a};return await this.outboxStore.enqueue(c),c}async start(t={}){this.runtimeStateStore.setState({phase:"starting",started:!1,syncing:!1,lastError:null,lastTransitionAt:new Date().toISOString()});try{await this.runtime.start(t),this.runtimeStateStore.setState(n=>({...n,phase:"started",started:!0,syncing:!1,startCount:n.startCount+1,lastError:null,lastTransitionAt:new Date().toISOString()}))}catch(n){throw this.runtimeStateStore.setState({phase:"error",started:!1,syncing:!1,lastError:n instanceof Error?n.message:String(n),lastTransitionAt:new Date().toISOString()}),n}}async stop(){this.stopScheduler(),this.runtimeStateStore.setState({phase:"stopping",started:this.runtime.isStarted(),syncing:!1,lastError:null,lastTransitionAt:new Date().toISOString()});try{await this.runtime.stop(),this.runtimeStateStore.setState(t=>({...t,phase:"stopped",started:!1,syncing:!1,stopCount:t.stopCount+1,lastError:null,lastTransitionAt:new Date().toISOString()}))}catch(t){throw this.runtimeStateStore.setState({phase:"error",started:this.runtime.isStarted(),syncing:!1,lastError:t instanceof Error?t.message:String(t),lastTransitionAt:new Date().toISOString()}),t}}async startSync(t){if(!this.runtime.isStarted())throw new Error("Cannot start sync before runtime is started");this.runtimeStateStore.setState({phase:"syncing",started:!0,syncing:!0,lastError:null,lastTransitionAt:new Date().toISOString()});try{if(this.transport){await this.processOutbox(t);let n=this.listDomainBridges();for(let r of n){let o=J(t,r),a=await this.checkpointStore.get(o),c;try{c=await this.transport.pull({scope:t,cursor:a,limit:this.policy.batchSize,domain:r})}catch(u){let i=(u==null?void 0:u.code)||"",s=u==null?void 0:u.details,p=Q(s==null?void 0:s.cutoffTime);if(i!=="SYNC_RESYNC_REQUIRED"||!p)throw u;await this.checkpointStore.set(o,p),a=p,c=await this.transport.pull({scope:t,cursor:a,limit:this.policy.batchSize,domain:r})}let l=(c.changes||[]).map(u=>{let i=String(u.domain||"").trim().toLowerCase();if(!i)return{...u,domain:r};if(i!==r)throw new Error(`Sync pull domain mismatch: expected ${r}, received ${i}`);return u});await this.applyRemoteChanges(l,t),c.nextCursor!==void 0&&await this.checkpointStore.set(o,c.nextCursor)}}this.runtimeStateStore.setState(n=>({...n,phase:"started",started:!0,syncing:!1,syncCount:n.syncCount+1,lastError:null,lastTransitionAt:new Date().toISOString()}))}catch(n){throw this.runtimeStateStore.setState({phase:"error",started:this.runtime.isStarted(),syncing:!1,lastError:n instanceof Error?n.message:String(n),lastTransitionAt:new Date().toISOString()}),n}}async stopSync(){this.runtimeStateStore.setState(t=>({...t,phase:this.runtime.isStarted()?"started":"stopped",syncing:!1,lastTransitionAt:new Date().toISOString()}))}isStarted(){return this.runtime.isStarted()}startScheduler(t){if(this.policy.scheduler.mode!=="interval")return;let n=this.policy.scheduler.intervalMs||1e4;this.stopScheduler(),this.schedulerTimer=setInterval(()=>{this.startSync(t).catch(r=>{this.runtimeStateStore.setState(o=>({...o,phase:"error",syncing:!1,lastError:r instanceof Error?r.message:String(r),lastTransitionAt:new Date().toISOString()}))})},n)}stopScheduler(){this.schedulerTimer&&(clearInterval(this.schedulerTimer),this.schedulerTimer=void 0)}async notifyOnline(t){this.policy.scheduler.onlineTrigger!==!1&&await this.startSync(t)}async processOutbox(t){var o;if(!this.transport)return;let n=await this.outboxStore.listPending(this.policy.batchSize);if(n.length===0)return;let r=new Map;for(let a of n){let c=String(a.change.domain||"").trim().toLowerCase()||"__unknown__";r.has(c)||r.set(c,[]),(o=r.get(c))==null||o.push(a)}for(let[a,c]of r.entries()){if(a==="__unknown__"){for(let p of c){let d=p.retryCount+1;await this.outboxStore.update({...p,retryCount:d,status:d>=this.policy.maxRetry?"FAILED":"PENDING",lastError:"DOMAIN_REQUIRED",updatedAt:new Date().toISOString()})}continue}let l=await this.transport.push({scope:t,outbox:c,domain:a}),u=new Set((l.applied||[]).map(p=>p.changeId)),i=new Map((l.failed||[]).map(p=>[p.changeId,p])),s=new Set((l.conflicts||[]).map(p=>p.changeId));await this.resolveConflicts(l.conflicts||[],t);for(let p of c){if(u.has(p.change.id)){await this.outboxStore.remove(p.outboxId);continue}let d=i.get(p.change.id);if(d||s.has(p.change.id)){let g=p.retryCount+1;await this.outboxStore.update({...p,retryCount:g,status:g>=this.policy.maxRetry?"FAILED":"PENDING",lastError:(d==null?void 0:d.message)||(s.has(p.change.id)?"SYNC_CONFLICT":p.lastError),updatedAt:new Date().toISOString()})}}}}async applyRemoteChanges(t,n){if(!Array.isArray(t)||t.length===0)return;let r=[...t].sort((a,c)=>a.occurredAt.localeCompare(c.occurredAt)),o=new Map;for(let a of r){o.has(a.domain)||o.set(a.domain,[]);let c=o.get(a.domain);c&&c.push(a)}for(let[a,c]of o.entries()){let l=this.bridges.get(a);if(l!=null&&l.applyRemoteChanges&&(await l.applyRemoteChanges(c,n),this.projectionHook))for(let u of c)await this.projectionHook(u,n)}}async resolveConflicts(t,n){var o;if(!((o=this.transport)!=null&&o.resolveConflicts)||t.length===0)return;let r=[];for(let a of t){let c=[...this.conflictResolvers],l=null;for(let u of c){let i=await u(a,{scope:n});if(i){l=i;break}}l||(l=P(a)),r.push(l)}r.length>0&&await this.transport.resolveConflicts({scope:n,resolutions:r})}},R=class{constructor(){this.runtimeBuilder=M.CoreRuntime.builder();this.options={};this.runtimeBuilder.withDbAdapter(()=>new B.InMemoryDbAdapter)}withPlatformAdapter(t){return this.runtimeBuilder.withPlatformAdapter(t),this}withDbAdapter(t){return this.runtimeBuilder.withDbAdapter(t),this}withHttpAdapter(t){return this.runtimeBuilder.withHttpAdapter(t),this}withSocketAdapter(t){return this.runtimeBuilder.withSocketAdapter(t),this}withLoggerAdapter(t){return this.runtimeBuilder.withLoggerAdapter(t),this}withActivitySink(t){return this.runtimeBuilder.withActivitySink(t),this}withExtension(t,n){return this.runtimeBuilder.withExtension(t,n),this}withPolicy(t){return this.options={...this.options,policy:t},this}withTransport(t){return this.options={...this.options,transport:t},this}withOutboxStore(t){return this.options={...this.options,outboxStore:t},this}withCheckpointStore(t){return this.options={...this.options,checkpointStore:t},this}withConflictResolvers(t){return this.options={...this.options,conflictResolvers:t},this}withProjectionHook(t){return this.options={...this.options,projectionHook:t},this}build(){let t=(0,x.createStore)({phase:"idle",started:!1,syncing:!1,startCount:0,stopCount:0,syncCount:0,lastError:null,lastTransitionAt:new Date().toISOString()}),n=this.runtimeBuilder.build();return new I(n,t,this.options)}};function V(e={}){let t=I.builder();return e.policy&&t.withPolicy(e.policy),e.transport&&t.withTransport(e.transport),e.outboxStore&&t.withOutboxStore(e.outboxStore),e.checkpointStore&&t.withCheckpointStore(e.checkpointStore),e.conflictResolvers&&t.withConflictResolvers(e.conflictResolvers),e.projectionHook&&t.withProjectionHook(e.projectionHook),t.build()}function Z(e,t){e.registerDomainBridge(t)}async function tt(e,t,n={}){e.isStarted()||await e.start(n),await e.startSync(t)}async function et(e){await e.stopSync()}var w=class extends Error{constructor(t,n,r=!1,o,a){super(n),this.name="OfsyncTransportError",this.code=t,this.retryable=r,this.status=o,this.details=a}};async function nt(e){let t=globalThis.crypto;if(!t||!t.subtle)throw new Error("WebCrypto API is not available");let n=new TextEncoder().encode(e),r=await t.subtle.digest("SHA-256",n);return[...new Uint8Array(r)].map(o=>o.toString(16).padStart(2,"0")).join("")}function rt(e){let t=String(e.table||""),n=String(e.id||""),r=String(e.changeId||"");return[t,n,r].join("::")}function F(e){return{conflictId:rt(e),changeId:String(e.changeId||""),domain:"unknown",entity:String(e.table||""),recordId:String(e.id||""),code:String(e.code||"UNKNOWN_CONFLICT"),message:String(e.message||"Conflict detected"),retryable:!!e.retryable,scope:{tenantId:typeof e.tenantId=="string"?e.tenantId:void 0,branchId:typeof e.branchId=="string"?e.branchId:void 0,ledgerProfileId:typeof e.ledgerProfileId=="string"?e.ledgerProfileId:typeof e.financeDomainId=="string"?e.financeDomainId:void 0},details:e}}function v(e){return!!(e&&typeof e=="object"&&!Array.isArray(e))}function ot(e){var n;let t=(n=e.branchId)==null?void 0:n.trim();if(!t)throw new w("BRANCH_SCOPE_REQUIRED","Sync scope requires branchId",!1);return t}var _=class{constructor(t){this.options=t}nowIso(){return this.options.nowIso?this.options.nowIso():new Date().toISOString()}defaultBootstrapSinceIso(){return new Date(Date.now()-2160*60*60*1e3).toISOString()}async buildHeaders(t){let n={"content-type":"application/json","x-branch-id":ot(t),"x-sync-protocol-version":"1"};if(t.tenantId&&(n["x-tenant-id"]=t.tenantId),t.deviceId&&(n["x-device-id"]=t.deviceId),t.ledgerProfileId&&(n["x-ledger-profile-id"]=t.ledgerProfileId),this.options.getAccessToken){let r=await this.options.getAccessToken();r&&(n.authorization=`Bearer ${r}`)}return n}normalizeUrl(t){return`${this.options.baseUrl.replace(/\/+$/,"")}${t}`}normalizeDomainPath(t){let n=t.trim().toLowerCase().replace(/[^a-z0-9_-]/g,"");return n||null}resolveDomainPathOrThrow(t){let n=this.normalizeDomainPath(t||"");if(!n)throw new w("DOMAIN_REQUIRED","Sync transport requires explicit domain",!1);return n}isGenericFallbackEnabled(){return this.options.allowGenericFallback!==!1}async requestWithDomainPolicy(t,n,r){if(n){let o=await this.options.httpAdapter.request({...t,url:this.normalizeUrl(n)});if(o.status!==404||!this.isGenericFallbackEnabled())return o}return this.options.httpAdapter.request({...t,url:this.normalizeUrl(r)})}parseFailure(t,n){var u,i,s;let r=n||{},o=((u=r.error)==null?void 0:u.code)||"SYNC_TRANSPORT_ERROR",a=((i=r.error)==null?void 0:i.message)||`Sync transport failed with status ${t}`,c=!!((s=r.error)!=null&&s.retryable),l=v(r.error)&&v(r.error.details)?r.error.details:void 0;throw new w(o,a,c,t,l)}async push(t){let n={changes:t.outbox.map(i=>{let s=v(i.change.payload)?i.change.payload:{},p=typeof s.table=="string"?s.table.trim():"",d=v(s.record)?s.record:null,g=Date.parse(i.change.occurredAt);return p&&d?{id:i.change.id,table:p,type:i.change.type,record:{...d,id:i.change.recordId||String(d.id||i.change.id)},updated_at:Number.isFinite(g)?g:i.change.occurredAt}:{id:i.change.id,entity:i.change.entity,type:i.change.type,data:{id:i.change.recordId,...s},timestamp:i.change.occurredAt}})},r=JSON.stringify(n),o=await nt(r),a=await this.buildHeaders(t.scope);a["x-data-checksum"]=o;let c=this.resolveDomainPathOrThrow(t.domain),l=await this.requestWithDomainPolicy({method:"POST",headers:a,body:n},`/sync/${c}/push`,"/sync/push");l.status>=400&&this.parseFailure(l.status,l.data);let u=l.data||{};return{applied:Array.isArray(u.applied)?u.applied:[],failed:Array.isArray(u.failed)?u.failed:[],conflicts:Array.isArray(u.conflicts)?u.conflicts.map(F):[]}}async pull(t){let n=await this.buildHeaders(t.scope),r=t.cursor||this.defaultBootstrapSinceIso(),o=this.resolveDomainPathOrThrow(t.domain),a=await this.requestWithDomainPolicy({method:"GET",headers:n,query:{since:r}},`/sync/${o}/pull`,"/sync/pull");a.status>=400&&this.parseFailure(a.status,a.data);let c=a.data||{},l=Array.isArray(c.changes)?c.changes.map(i=>{var d,g,f,h;let s=typeof i.table=="string"?i.table:"",p=typeof i.domain=="string"&&i.domain.trim().length>0?i.domain:s.startsWith("auth_")?"ofauth":s.startsWith("offinance_")?"offinance":s.startsWith("ofcoop_")?"ofcoop":s.startsWith("ofpos_")?"ofpos":"unknown";return{id:String(i.changeId||i.id||""),domain:String(p),entity:String(i.entity||s||""),type:String(i.type||i.action||"UPDATE"),recordId:String(i.recordId||((d=i.record)==null?void 0:d.id)||((g=i.data)==null?void 0:g.id)||i.id||""),payload:typeof s=="string"&&s.length>0?{table:String(s),action:String(i.action||i.type||"UPDATE"),record:{...i.record||i.data||{},id:String(i.recordId||((f=i.record)==null?void 0:f.id)||((h=i.data)==null?void 0:h.id)||i.id||"")}}:i.record||i.data||{},occurredAt:String(i.timestamp||this.nowIso()),scope:t.scope}}):[],u=l.length>0?l[l.length-1].occurredAt:this.nowIso();return{changes:l,nextCursor:u,conflicts:[]}}async listConflicts(t){let n=await this.buildHeaders(t.scope),r=await this.options.httpAdapter.request({method:"GET",url:this.normalizeUrl("/sync/conflicts"),headers:n});r.status>=400&&this.parseFailure(r.status,r.data);let o=r.data||{};return Array.isArray(o.conflicts)?o.conflicts.map(F):[]}async resolveConflicts(t){var i;let n=await this.listConflicts({scope:t.scope}),r=new Map(n.map(s=>[s.conflictId,s])),o=t.resolutions.map(s=>r.get(s.conflictId)).filter(s=>!!s).map(s=>({table:s.entity,id:s.recordId})),a=await this.buildHeaders(t.scope),c={resolutions:o},l=await this.options.httpAdapter.request({method:"POST",url:this.normalizeUrl("/sync/conflicts/resolve"),headers:a,body:c});l.status>=400&&this.parseFailure(l.status,l.data);let u=Array.isArray((i=l.data)==null?void 0:i.remaining)?l.data.remaining.length:0;return{resolvedCount:Math.max(0,o.length-u)}}};function U(e){return`Tidak dapat terhubung ke server sinkronisasi (${e.trim()||"VITE_OFSYNC_BASE_URL"}). Pastikan ofsync-server berjalan dan CORS origin browser diizinkan.`}function it(e){return e.trim().replace(/\/+$/,"")}function st(e){return e instanceof Error?e.message:String(e!=null?e:"")}function H(e,t=9e5){if(typeof e=="number"&&Number.isFinite(e)&&e>0)return Math.floor(e*1e3);if(typeof e!="string")return t;let n=e.trim();if(!n)return t;if(/^\d+$/.test(n))return Math.max(1,Number(n))*1e3;let r=n.match(/^(\d+)\s*([smhd])$/i);if(!r)return t;let o=Number(r[1]),a=r[2].toLowerCase(),c=a==="s"?1e3:a==="m"?6e4:a==="h"?36e5:864e5;return Math.max(1,o)*c}function E(e,t,n=U){let r=st(e),o=r.toLowerCase();return o.includes("networkerror")||o.includes("failed to fetch")||o.includes("load failed")||o.includes("fetch failed")||o.includes("the network connection was lost")||o.includes("network request failed")?n(t):r}function S(e,t){let n=new Error(t);return n.code=e,n}async function at(e){var o,a,c,l;if(e.bearerToken.length>0)return e.bearerToken;let t=(o=e.nowMs)!=null?o:Date.now(),n=(a=e.refreshLeewayMs)!=null?a:1e4;if((c=e.session)!=null&&c.accessToken){if(e.session.expiresAtMs>t+n)return e.session.accessToken;let u=await e.refresh();if(u!=null&&u.accessToken)return u.accessToken}let r=await e.loginFromCredentials();return(l=r==null?void 0:r.accessToken)!=null?l:""}function ct(e){var l,u,i;let t=(l=e.fetchImpl)!=null?l:fetch,n=it(e.baseUrl),r=(u=e.accessTokenFallbackTtlMs)!=null?u:9e5,o=(i=e.networkErrorMessage)!=null?i:U,a=s=>(Array.isArray(s.scopes)?s.scopes:[]).filter(d=>!!(d&&typeof d=="object")).map(d=>{var g,f,h,y;return{tenantId:typeof d.tenantId=="string"&&d.tenantId.trim().length>0?d.tenantId:null,branchId:String((g=d.branchId)!=null?g:"").trim(),role:String((f=d.role)!=null?f:""),userId:String((h=d.userId)!=null?h:""),email:String((y=d.email)!=null?y:"")}}).filter(d=>d.branchId.length>0),c=(s,p)=>{var f,h,y;let d=String((h=(f=s.accessToken)!=null?f:s.token)!=null?h:""),g=String((y=s.refreshToken)!=null?y:"");if(!d||!g)throw S("SYNC_AUTH_TOKEN_INCOMPLETE","Sync auth gagal: token tidak lengkap.");return{accessToken:d,refreshToken:g,expiresAtMs:Date.now()+H(s.expiresIn,r),principal:p}};return{async fetchScopeOptions(s,p){var d,g,f;if(!n)return[];try{let h=await t(`${n}/api/v1/auth/scope-options`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:s,password:p})}),y=await h.json().catch(()=>({}));if(!h.ok){let m=(d=y.error)!=null?d:{};throw S(String((g=m.code)!=null?g:`HTTP_${h.status}`),String((f=m.message)!=null?f:`Sync scope discovery gagal (${h.status})`))}return a(y)}catch(h){let y=h;throw S(typeof(y==null?void 0:y.code)=="string"?y.code:"SYNC_AUTH_SCOPE_DISCOVERY_FAILED",E(h,n,o))}},async login(s,p,d){var h,y,m,D,L;let g=String((h=d.branchId)!=null?h:"").trim(),f=String((y=d.tenantId)!=null?y:"").trim();try{let b=await t(`${n}/api/v1/auth/login`,{method:"POST",headers:{"Content-Type":"application/json","X-Branch-ID":g,...f?{"X-Tenant-ID":f}:{},"X-Device-ID":e.deviceId},body:JSON.stringify({username:s,password:p})}),C=await b.json().catch(()=>({}));if(!b.ok){let N=(m=C.error)!=null?m:{};throw S(String((D=N.code)!=null?D:`HTTP_${b.status}`),String((L=N.message)!=null?L:`Sync auth login gagal (${b.status})`))}return c(C,s)}catch(b){let C=b;throw S(typeof(C==null?void 0:C.code)=="string"?C.code:"SYNC_AUTH_FAILED",E(b,n,o))}},async refresh(s,p){var d,g,f;try{let h=await t(`${n}/api/v1/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json",...p?{"X-Tenant-ID":p}:{},"X-Device-ID":e.deviceId},body:JSON.stringify({refreshToken:s})}),y=await h.json().catch(()=>({}));if(!h.ok){let m=(d=y.error)!=null?d:{};throw S(String((g=m.code)!=null?g:`HTTP_${h.status}`),String((f=m.message)!=null?f:`Sync auth refresh gagal (${h.status})`))}return c(y,"")}catch(h){let y=h;throw S(typeof(y==null?void 0:y.code)=="string"?y.code:"SYNC_AUTH_REFRESH_FAILED",E(h,n,o))}}}}
|
|
@@ -3,6 +3,7 @@ export interface OutboxStore {
|
|
|
3
3
|
enqueue(item: OutboxItem): Promise<void>;
|
|
4
4
|
listPending(limit: number): Promise<OutboxItem[]>;
|
|
5
5
|
findByIdempotencyKey(idempotencyKey: string): Promise<OutboxItem | null>;
|
|
6
|
+
listAll(): Promise<OutboxItem[]>;
|
|
6
7
|
update(item: OutboxItem): Promise<void>;
|
|
7
8
|
remove(outboxId: string): Promise<void>;
|
|
8
9
|
}
|
|
@@ -11,6 +12,7 @@ export declare class InMemoryOutboxStore implements OutboxStore {
|
|
|
11
12
|
enqueue(item: OutboxItem): Promise<void>;
|
|
12
13
|
listPending(limit: number): Promise<OutboxItem[]>;
|
|
13
14
|
findByIdempotencyKey(idempotencyKey: string): Promise<OutboxItem | null>;
|
|
15
|
+
listAll(): Promise<OutboxItem[]>;
|
|
14
16
|
update(item: OutboxItem): Promise<void>;
|
|
15
17
|
remove(outboxId: string): Promise<void>;
|
|
16
18
|
}
|
|
@@ -21,11 +21,13 @@ export interface SyncTransport {
|
|
|
21
21
|
push(params: {
|
|
22
22
|
scope: SyncScope;
|
|
23
23
|
outbox: OutboxItem[];
|
|
24
|
+
domain: string;
|
|
24
25
|
}): Promise<SyncPushResult>;
|
|
25
26
|
pull(params: {
|
|
26
27
|
scope: SyncScope;
|
|
27
28
|
cursor: string | null;
|
|
28
29
|
limit: number;
|
|
30
|
+
domain: string;
|
|
29
31
|
}): Promise<SyncPullResult>;
|
|
30
32
|
listConflicts?(params: {
|
|
31
33
|
scope: SyncScope;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofsync-shared-core",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Host-side offline-first sync orchestration core for multi-domain of* apps.",
|
|
6
6
|
"author": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"bundle": "node esbuild.config.js"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"ofcore": "0.
|
|
38
|
+
"ofcore": "0.2.0-alpha.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"esbuild": "^0.25.11",
|