@interncom/diplomatic 0.4.5 → 0.6.0

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.
@@ -39,11 +39,32 @@ export type IOp = IDeleteOp | IMutateOp;
39
39
  export declare function isDeleteOp(op: IOp): op is IDeleteOp;
40
40
  export declare function isMutateOp(op: IOp): op is IMutateOp;
41
41
  export interface IInsertParams<T> extends IMsgEntBody<T> {
42
+ /** Optional 8-byte id material for the new eid; else random. */
42
43
  id?: Uint8Array;
43
44
  }
44
- export interface IUpsertParams<T> extends IMsgEntBody<T> {
45
- eid?: EntityID;
46
- }
45
+ /**
46
+ * Revision of an ent used as the base for update/delete.
47
+ * Apps almost always already hold this (the row being edited).
48
+ */
49
+ export interface IEntRev {
50
+ eid: EntityID;
51
+ ctr: number;
52
+ /** Last-write time of this rev (eid.ts + off of the latest msg for this eid). */
53
+ updatedAt: Date;
54
+ }
55
+ export interface IUpdateParams<T> extends IMsgEntBody<T> {
56
+ prior: IEntRev;
57
+ /** Clock-skew recovery; default client-wide (usually true). */
58
+ force?: boolean;
59
+ }
60
+ /** Prefer `{ prior }`; `{ eid }` loads prior from the message archive. */
61
+ export type IDeleteParams = {
62
+ prior: IEntRev;
63
+ force?: boolean;
64
+ } | {
65
+ eid: EntityID;
66
+ force?: boolean;
67
+ };
47
68
  export interface IStorage {
48
69
  addUser: (pubKey: PublicKey) => Promise<ValStat<void>>;
49
70
  hasUser: (pubKey: PublicKey) => Promise<ValStat<boolean>>;
@@ -172,6 +193,12 @@ export interface IStateManager {
172
193
  * updated shared application state in IndexedDB).
173
194
  */
174
195
  notify: (types: Iterable<string>) => void;
196
+ /**
197
+ * Peer wrote durable application state for these eids (e.g. sync worker).
198
+ * With CachedEntDB: pull those eids from durable; type-level UI notifies
199
+ * fire only if mem changed.
200
+ */
201
+ refresh: (eids: Iterable<EntityID>) => Promise<void>;
175
202
  on: (type: string, listener: () => void) => void;
176
203
  off: (type: string, listener: () => void) => void;
177
204
  }
@@ -1,23 +1,42 @@
1
1
  import { Status } from "./shared/consts";
2
- import { IMessage, IMsgEntBody, IOp, IStateManager } from "./shared/types";
2
+ import { EntityID, IMessage, IMsgEntBody, IOp, IStateManager } from "./shared/types";
3
3
  import { ValStat } from "./shared/valstat";
4
4
  import type { Applier } from "./types";
5
5
  export declare function isMsgEntBody(bodDec: unknown): bodDec is IMsgEntBody;
6
6
  export declare function msgToOp(msg: IMessage): ValStat<IOp>;
7
+ type PeerIngestFn = (eids: Iterable<EntityID>) => Promise<void>;
7
8
  export declare class StateManager implements IStateManager {
8
9
  applier: Applier;
9
10
  private emitter;
10
11
  private clearer;
11
- private onTypes;
12
+ /** Worker / peer: eids successfully applied to durable EntDB. */
13
+ private onDirtyEids;
14
+ /**
15
+ * When set (CachedEntDB), type events are driven by the cache's subscribe
16
+ * path (immediate + durable ingest). apply() does not re-emit.
17
+ */
18
+ private cacheDriven;
19
+ private peerIngest;
12
20
  constructor(applier: Applier, clearer: () => Promise<Status>,
13
- /** Optional hook after a successful apply batch (e.g. worker dirty signal). */
14
- onTypes?: (types: Set<string>) => void);
21
+ /**
22
+ * Called with eids that successfully applied (e.g. worker posts dirty).
23
+ */
24
+ onDirtyEids?: (eids: EntityID[]) => void, opts?: {
25
+ cacheDriven?: boolean;
26
+ peerIngest?: PeerIngestFn;
27
+ });
15
28
  apply: (msgs: IMessage[]) => Promise<Status[]>;
16
29
  /** Clear application state and notify all type subscribers. */
17
30
  clear: () => Promise<Status>;
18
- /** Notify type subscribers without applying msgs (shared-IDB peer updates). */
31
+ /** Notify type subscribers without applying msgs. */
19
32
  notify: (types: Iterable<string>) => void;
33
+ /**
34
+ * Peer (e.g. sync worker) updated durable EntDB for these eids.
35
+ * Cache pulls those rows; apps still hear type-level events via subscribe.
36
+ */
37
+ refresh: (eids: Iterable<EntityID>) => Promise<void>;
20
38
  on: (opType: string, listener: () => void) => void;
21
39
  off: (opType: string, listener: () => void) => void;
22
40
  }
23
41
  export declare const nullStateManager: IStateManager;
42
+ export {};
@@ -1,7 +1,7 @@
1
1
  import { Status } from "../../shared/consts";
2
2
  import { ICrypto } from "../../shared/types";
3
3
  import { EntityID, Hash } from "../../shared/types";
4
- import { IMessageStore, IStorableMessage, IStoredMessage } from "../../types";
4
+ import { ApldState, IMessageStore, IStorableMessage, IStoredMessage } from "../../types";
5
5
  export declare class IDBMessageStore implements IMessageStore {
6
6
  private crypto;
7
7
  db: IDBDatabase;
@@ -10,9 +10,12 @@ export declare class IDBMessageStore implements IMessageStore {
10
10
  del(keys: Iterable<Hash>): Promise<void>;
11
11
  get(key: Hash): Promise<IStoredMessage | undefined>;
12
12
  has(key: Hash): Promise<boolean>;
13
- list(): Promise<Iterable<IStoredMessage>>;
13
+ list(apld?: ApldState): Promise<IStoredMessage[]>;
14
14
  last(eid: EntityID): Promise<IStoredMessage | undefined>;
15
- listUnapplied(): Promise<IStoredMessage[]>;
16
15
  markApplied(keys: Iterable<Hash>): Promise<void>;
16
+ markFailed(entries: Iterable<{
17
+ key: Hash;
18
+ err: Status;
19
+ }>): Promise<void>;
17
20
  wipe(): Promise<void>;
18
21
  }
@@ -11,8 +11,9 @@ export declare const UPLOAD_QUEUE_TABLE = "uploadQueue";
11
11
  export declare const DOWNLOAD_QUEUE_TABLE = "downloadQueue";
12
12
  export declare const MESSAGES_TABLE = "messages";
13
13
  /**
14
- * Index on messages.apld. Values are "f" (pending) / "t" (applied)
15
- * booleans are not valid IndexedDB keys; single-char strings keep keys compact.
14
+ * Index on messages.apld ({@link APLD_PENDING} / {@link APLD_APPLIED} /
15
+ * {@link APLD_ERROR}). Booleans are not valid IndexedDB keys; single-char
16
+ * strings keep keys compact.
16
17
  */
17
18
  export declare const MESSAGES_APLD_INDEX = "apld";
18
19
  /** Schema version: v3 adds messages.apld index for the apply queue. */
@@ -1,6 +1,6 @@
1
1
  import { ICrypto } from "../../shared/types";
2
2
  import { EntityID, Hash } from "../../shared/types";
3
- import { IMessageStore, IStorableMessage, IStoredMessage, IStoredMessageData } from "../../types";
3
+ import { ApldState, IMessageStore, IStorableMessage, IStoredMessage, IStoredMessageData } from "../../types";
4
4
  import { Status } from "../../shared/consts";
5
5
  export declare class MemoryMessageStore implements IMessageStore {
6
6
  private crypto;
@@ -10,9 +10,12 @@ export declare class MemoryMessageStore implements IMessageStore {
10
10
  del(keys: Iterable<Hash>): Promise<void>;
11
11
  get(key: Hash): Promise<IStoredMessage | undefined>;
12
12
  has(key: Hash): Promise<boolean>;
13
- list(): Promise<Iterable<IStoredMessage>>;
13
+ list(apld?: ApldState): Promise<IStoredMessage[]>;
14
14
  last(eid: EntityID): Promise<IStoredMessage | undefined>;
15
- listUnapplied(): Promise<IStoredMessage[]>;
16
15
  markApplied(keys: Iterable<Hash>): Promise<void>;
16
+ markFailed(entries: Iterable<{
17
+ key: Hash;
18
+ err: Status;
19
+ }>): Promise<void>;
17
20
  wipe(): Promise<void>;
18
21
  }
@@ -2,7 +2,7 @@ import type { SyncProgressEvent } from "./progress";
2
2
  import { Status } from "./shared/consts";
3
3
  import type { Enclave } from "./shared/enclave";
4
4
  import type { EncodedMessage } from "./shared/message";
5
- import type { EntityID, Hash, HostHandle, IHostConnectionInfo, IHostMetadata, IInsertParams, IMessageHead, IOp, IUpsertParams, MasterSeed, SerializedContent } from "./shared/types";
5
+ import type { EntityID, Hash, HostHandle, IDeleteParams, IEntRev, IHostConnectionInfo, IHostMetadata, IInsertParams, IMessageHead, IOp, IUpdateParams, MasterSeed, SerializedContent } from "./shared/types";
6
6
  import { ValStat } from "./shared/valstat";
7
7
  import { ICrypto } from "./shared/types";
8
8
  export interface IMsgParts {
@@ -27,6 +27,7 @@ export interface IDiplomaticClientXferState {
27
27
  export type Applier = (ops: IOp[]) => Promise<{
28
28
  stats: Status[];
29
29
  types: Set<string>;
30
+ eids: EntityID[];
30
31
  }>;
31
32
  export interface ISeedStore {
32
33
  save: (seed: MasterSeed) => Promise<Enclave>;
@@ -74,10 +75,18 @@ export interface IDownloadQueue {
74
75
  count: () => Promise<number>;
75
76
  wipe(): Promise<void>;
76
77
  }
78
+ /**
79
+ * Apply lifecycle values for archive rows (IDB-indexable single-char strings;
80
+ * booleans are not valid IndexedDB keys). Defined first so {@link ApldState}
81
+ * is only those three literals.
82
+ */
83
+ export declare const APLD_PENDING: "f";
84
+ export declare const APLD_APPLIED: "t";
85
+ export declare const APLD_ERROR: "e";
86
+ /** Only {@link APLD_PENDING}, {@link APLD_APPLIED}, or {@link APLD_ERROR}. */
87
+ export type ApldState = typeof APLD_PENDING | typeof APLD_APPLIED | typeof APLD_ERROR;
77
88
  /**
78
89
  * Archive fields shared by read and write.
79
- * `apld`: true once the msg has been applied by the application state manager;
80
- * false while still pending apply.
81
90
  */
82
91
  export interface IStoredMessageFields {
83
92
  eid: EntityID;
@@ -87,19 +96,23 @@ export interface IStoredMessageFields {
87
96
  }
88
97
  /**
89
98
  * What may come back from storage (pre-apld rows can omit the field).
90
- * IDB stores "t"|"f" (booleans are not valid IndexedDB index keys).
91
- * Prefer {@link normalizeStoredMessageData} before use.
99
+ * Typed rows use only {@link ApldState}; {@link apldFromStored} coerces
100
+ * legacy boolean / unknown values at the storage boundary.
92
101
  */
93
102
  export interface IStoredMessageData extends IStoredMessageFields {
94
- apld?: boolean | "t" | "f";
103
+ apld?: ApldState;
104
+ /** Status code when apld is {@link APLD_ERROR}. */
105
+ err?: number;
95
106
  }
96
107
  /**
97
108
  * Required shape for every put into the message archive (app/API layer).
98
- * Callers must set `apld` (false until applied, then true).
99
- * The IDB adapter persists this as "t"|"f" for indexing.
109
+ * `apld` is required and must be one of the three {@link ApldState} values.
110
+ * Omit optional fields (`off`, `ctr`, `body`, `err`) rather than storing empties.
100
111
  */
101
112
  export type IStoredMessageWrite = IStoredMessageFields & {
102
- apld: boolean;
113
+ apld: ApldState;
114
+ /** Status code when apld is {@link APLD_ERROR}. */
115
+ err?: number;
103
116
  };
104
117
  export interface IStorableMessage {
105
118
  key: Hash;
@@ -109,29 +122,51 @@ export interface IStoredMessage {
109
122
  hash: Hash;
110
123
  head: IMessageHead;
111
124
  body?: EncodedMessage;
112
- applied: boolean;
125
+ /** Apply lifecycle — same {@link ApldState} as the archive row. */
126
+ apld: ApldState;
127
+ /** Status code when apld is {@link APLD_ERROR}. */
128
+ err?: number;
113
129
  }
130
+ /** True only for the three legal {@link ApldState} values. */
131
+ export declare function isApldState(v: unknown): v is ApldState;
114
132
  /**
115
- * Coerce stored `apld` to boolean.
116
- * Applied: true | "t". Pending: false | "f" | missing.
133
+ * Coerce raw storage values to {@link ApldState}.
134
+ * Applied: true | APLD_APPLIED. Failed: APLD_ERROR.
135
+ * Pending: false | APLD_PENDING | missing | anything else.
117
136
  */
118
- export declare function apldFromStored(v: unknown): boolean;
119
- /** Coerce storage rows to the write shape with boolean apld. */
120
- export declare function normalizeStoredMessageData(data: IStoredMessageData): IStoredMessageWrite;
121
- /** Pending apply when not yet marked applied. */
137
+ export declare function apldFromStored(v: unknown): ApldState;
138
+ /** Pending apply when not yet applied or terminally failed. */
122
139
  export declare function isPendingApply(data: IStoredMessageData): boolean;
140
+ /**
141
+ * Apply failures that should not be retried (poison / protocol / shape).
142
+ * Transient storage errors stay pending ({@link APLD_PENDING}) for a later drain.
143
+ */
144
+ export declare function isTerminalApplyFailure(st: Status): boolean;
145
+ /**
146
+ * Mutate a stored row's apply state in place (no copy).
147
+ * Clears `err` unless setting {@link APLD_ERROR}.
148
+ */
149
+ export declare function setApld(data: IStoredMessageData, apld: ApldState, err?: Status): void;
123
150
  export declare function toStoredMessage(hash: Hash, data: IStoredMessageData, crypto: ICrypto): Promise<IStoredMessage>;
124
151
  export interface IMessageStore {
125
152
  add: (messages: IStorableMessage[]) => Promise<Status[]>;
126
153
  get: (key: Hash) => Promise<IStoredMessage | undefined>;
127
154
  has: (key: Hash) => Promise<boolean>;
128
155
  del: (keys: Iterable<Hash>) => Promise<void>;
129
- list: () => Promise<Iterable<IStoredMessage>>;
156
+ /**
157
+ * List archive rows. Pass {@link ApldState} to filter by apply lifecycle
158
+ * (e.g. {@link APLD_PENDING} for the apply queue, {@link APLD_ERROR} for
159
+ * diagnostics). Omit for all messages.
160
+ */
161
+ list: (apld?: ApldState) => Promise<IStoredMessage[]>;
130
162
  last: (eid: EntityID) => Promise<IStoredMessage | undefined>;
131
- /** Messages stored but not yet applied (apld === false). */
132
- listUnapplied: () => Promise<IStoredMessage[]>;
133
- /** Mark archive rows as applied (apld = true). */
163
+ /** Mark archive rows as applied ({@link APLD_APPLIED}). */
134
164
  markApplied: (keys: Iterable<Hash>) => Promise<void>;
165
+ /** Mark archive rows as terminal apply failure ({@link APLD_ERROR}). */
166
+ markFailed: (entries: Iterable<{
167
+ key: Hash;
168
+ err: Status;
169
+ }>) => Promise<void>;
135
170
  wipe(): Promise<void>;
136
171
  }
137
172
  export interface IStore<Handle extends HostHandle> {
@@ -157,10 +192,13 @@ export interface IClient<Handle extends HostHandle> {
157
192
  connect(): Promise<void>;
158
193
  disconnect(): Promise<void>;
159
194
  insertRaw(content: SerializedContent): Promise<ValStat<IMessageHead>>;
160
- upsertRaw(eid: EntityID, content: SerializedContent, force?: boolean): Promise<ValStat<IMessageHead>>;
195
+ updateRaw(prior: IEntRev, content: SerializedContent | undefined, force?: boolean): Promise<ValStat<IMessageHead>>;
161
196
  insert<T = unknown>(op: IInsertParams<T>): Promise<ValStat<IMessageHead>>;
162
- upsert<T = unknown>(op: IUpsertParams<T>, force?: boolean): Promise<ValStat<IMessageHead>>;
163
- delete(eid: EntityID): Promise<ValStat<IMessageHead>>;
197
+ update<T = unknown>(op: IUpdateParams<T>): Promise<ValStat<IMessageHead>>;
198
+ /**
199
+ * Delete by `{ prior }` (preferred) or `{ eid }` (archive lookup).
200
+ */
201
+ delete(op: IDeleteParams): Promise<ValStat<IMessageHead>>;
164
202
  /** Allocate an entity id (optional 8-byte id material; else random). */
165
203
  genEID(id?: Uint8Array): Promise<ValStat<EntityID>>;
166
204
  sync(): Promise<Status>;
@@ -1,6 +1,6 @@
1
1
  import { IClock } from "../shared/clock";
2
2
  import { Status } from "../shared/consts";
3
- import type { EntityID, IHostConnectionInfo, IInsertParams, IMessageHead, IStateManager, IUpsertParams, MasterSeed, SerializedContent } from "../shared/types";
3
+ import type { EntityID, IDeleteParams, IEntRev, IHostConnectionInfo, IInsertParams, IMessageHead, IStateManager, IUpdateParams, MasterSeed, SerializedContent } from "../shared/types";
4
4
  import type { ValStat } from "../shared/valstat";
5
5
  import type { IClient, IDiplomaticClientState, IDiplomaticClientXferState, IHostRow, IStateEmitter, IStore } from "../types";
6
6
  /**
@@ -13,6 +13,11 @@ import type { IClient, IDiplomaticClientState, IDiplomaticClientXferState, IHost
13
13
  * connect also probes with `ping`. Early construction (module scope) is fine even
14
14
  * if `ready` fired before `onmessage` was set — the ping still succeeds once the
15
15
  * worker has finished init (cmds are held until then on the worker side).
16
+ *
17
+ * After the ready barrier, connect hydrates `clientState` / `xferState` from the
18
+ * shared main-thread store (and recovers via getClientState/getXferState if the
19
+ * unsolicited events were dropped). Without that, the façade defaults to
20
+ * `hasSeed: false` and apps flash the unauthenticated UI until a later event.
16
21
  */
17
22
  export type WorkerClientOptions = {
18
23
  /** Already-constructed module Worker running `@interncom/diplomatic/worker`. */
@@ -60,8 +65,23 @@ export declare class WorkerClient implements IClient<URL> {
60
65
  * Handshake: wait for unsolicited `ready` **or** a successful probe `ping`.
61
66
  * The probe covers the common case where the app started the Worker early and
62
67
  * `ready` was dropped before this thread set `onmessage`.
68
+ *
69
+ * Then hydrate client/xfer state from the shared store (and RPC) so
70
+ * `clientState.get()` is correct before connect returns — unsolicited
71
+ * `clientState` events are often lost when the Worker starts before
72
+ * `onmessage` is attached.
63
73
  */
64
74
  static connect(state: IStateManager, store: IStore<URL>, opts: WorkerClientOptions): Promise<WorkerClient>;
75
+ /**
76
+ * Snapshot seed/host/queues from the shared main-thread store.
77
+ * `connected` stays false until the worker reports otherwise.
78
+ */
79
+ private hydrateStateFromStore;
80
+ /**
81
+ * Request current client/xfer state from the worker. Recovers when unsolicited
82
+ * `clientState` / `xferState` events fired before `onmessage` was set.
83
+ */
84
+ private pullRemoteState;
65
85
  /** Resolve the ready barrier (idempotent). */
66
86
  private markReady;
67
87
  /**
@@ -85,12 +105,12 @@ export declare class WorkerClient implements IClient<URL> {
85
105
  hosts(): Promise<IHostRow<URL>[]>;
86
106
  connect(listen?: boolean, sync?: boolean): Promise<void>;
87
107
  disconnect(): Promise<void>;
88
- /** Local UI write: archive + apply on main (fast UI); upload/sync via worker. */
108
+ /** Local UI write: archive + apply on main (cache notifies UI); sync via worker. */
89
109
  insertRaw(content: SerializedContent): Promise<ValStat<IMessageHead>>;
90
- upsertRaw(eid: EntityID, content: SerializedContent | undefined, force?: boolean): Promise<ValStat<IMessageHead>>;
110
+ updateRaw(prior: IEntRev, content: SerializedContent | undefined, force?: boolean): Promise<ValStat<IMessageHead>>;
91
111
  insert<T = unknown>(op: IInsertParams<T>): Promise<ValStat<IMessageHead>>;
92
- upsert<T = unknown>(op: IUpsertParams<T>, force?: boolean): Promise<ValStat<IMessageHead>>;
93
- delete(eid: EntityID): Promise<ValStat<IMessageHead>>;
112
+ update<T = unknown>(op: IUpdateParams<T>): Promise<ValStat<IMessageHead>>;
113
+ delete(op: IDeleteParams): Promise<ValStat<IMessageHead>>;
94
114
  genEID(id?: Uint8Array): Promise<ValStat<EntityID>>;
95
115
  sync(): Promise<Status>;
96
116
  wipe(): Promise<void>;
@@ -1,5 +1,4 @@
1
1
  import type { Status } from "../shared/consts";
2
- import type { IMessageHead } from "../shared/types";
3
2
  import type { SyncProgressEvent } from "../progress";
4
3
  import type { IDiplomaticClientState, IDiplomaticClientXferState } from "../types";
5
4
  /** Host identity as plain data (URL → string for structured clone). */
@@ -38,40 +37,6 @@ export type WorkerCmd = {
38
37
  } | {
39
38
  id: number;
40
39
  op: "wipe";
41
- } | {
42
- id: number;
43
- op: "insertRaw";
44
- body: Uint8Array;
45
- } | {
46
- id: number;
47
- op: "upsertRaw";
48
- eid: Uint8Array;
49
- body?: Uint8Array;
50
- force?: boolean;
51
- } | {
52
- id: number;
53
- op: "insert";
54
- params: {
55
- type: string;
56
- body: unknown;
57
- gid?: string;
58
- pid?: Uint8Array;
59
- };
60
- } | {
61
- id: number;
62
- op: "upsert";
63
- params: {
64
- type: string;
65
- body: unknown;
66
- eid?: Uint8Array;
67
- gid?: string;
68
- pid?: Uint8Array;
69
- };
70
- force?: boolean;
71
- } | {
72
- id: number;
73
- op: "delete";
74
- eid: Uint8Array;
75
40
  } | {
76
41
  id: number;
77
42
  op: "import";
@@ -108,17 +73,19 @@ export type WorkerEvent = {
108
73
  kind: "xferState";
109
74
  state: IDiplomaticClientXferState;
110
75
  }
111
- /** Application state (e.g. EntDB) changed for these op types; re-read IDB. */
76
+ /**
77
+ * Application state (e.g. EntDB) changed for these eids.
78
+ * Main cache pulls only those rows from shared IDB, then notifies by type.
79
+ */
112
80
  | {
113
81
  kind: "dirty";
114
- types: string[];
82
+ eids: Uint8Array[];
115
83
  } | {
116
84
  kind: "wiped";
117
85
  } | WorkerReply;
118
86
  export declare function isWorkerEvent(data: unknown): data is WorkerEvent;
119
87
  export declare function isWorkerCmd(data: unknown): data is WorkerCmd;
120
88
  /** Narrow helpers for typed replies (avoid casts at call sites). */
121
- export declare function headFromUnknown(v: unknown): IMessageHead | undefined;
122
89
  export declare function statusFromUnknown(v: unknown): Status | undefined;
123
90
  export declare function clientStateFromUnknown(v: unknown): IDiplomaticClientState | undefined;
124
91
  export declare function progressFromUnknown(v: unknown): SyncProgressEvent | undefined;