@abloatai/ablo 0.27.0 → 0.28.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.
- package/CHANGELOG.md +29 -3
- package/README.md +1 -1
- package/dist/BaseSyncedStore.js +8 -9
- package/dist/Database.d.ts +14 -1
- package/dist/Database.js +225 -28
- package/dist/Model.d.ts +17 -0
- package/dist/Model.js +32 -1
- package/dist/SyncClient.d.ts +10 -6
- package/dist/SyncClient.js +379 -76
- package/dist/adapters/inMemoryStorage.d.ts +1 -0
- package/dist/adapters/inMemoryStorage.js +12 -0
- package/dist/cli.cjs +6 -2
- package/dist/client/Ablo.d.ts +24 -1
- package/dist/client/Ablo.js +4 -3
- package/dist/client/ApiClient.js +309 -43
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +1 -1
- package/dist/client/httpClient.d.ts +2 -0
- package/dist/client/modelRegistration.js +11 -0
- package/dist/client/options.d.ts +23 -0
- package/dist/client/wsMutationExecutor.d.ts +3 -2
- package/dist/client/wsMutationExecutor.js +3 -2
- package/dist/commit/contract.d.ts +493 -0
- package/dist/commit/contract.js +187 -0
- package/dist/commit/index.d.ts +6 -0
- package/dist/commit/index.js +5 -0
- package/dist/core/StoreManager.d.ts +2 -0
- package/dist/core/StoreManager.js +12 -0
- package/dist/errorCodes.js +6 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2 -0
- package/dist/interfaces/index.d.ts +2 -2
- package/dist/mutators/UndoManager.d.ts +2 -0
- package/dist/mutators/UndoManager.js +32 -0
- package/dist/react/useAblo.d.ts +6 -4
- package/dist/react/useAblo.js +25 -3
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/schema.d.ts +31 -1
- package/dist/stores/ObjectStore.d.ts +14 -1
- package/dist/stores/ObjectStore.js +27 -4
- package/dist/stores/ObjectStoreContract.d.ts +2 -0
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +2 -0
- package/dist/sync/SyncWebSocket.d.ts +2 -1
- package/dist/sync/persistedPrefix.d.ts +12 -0
- package/dist/sync/persistedPrefix.js +22 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/mocks/FakeDatabase.d.ts +18 -0
- package/dist/testing/mocks/FakeDatabase.js +10 -0
- package/dist/testing/mocks/MockWebSocket.d.ts +2 -1
- package/dist/transactions/TransactionQueue.d.ts +66 -8
- package/dist/transactions/TransactionQueue.js +607 -89
- package/dist/transactions/commitEnvelope.d.ts +132 -0
- package/dist/transactions/commitEnvelope.js +139 -0
- package/dist/transactions/commitOutboxStore.d.ts +32 -0
- package/dist/transactions/commitOutboxStore.js +26 -0
- package/dist/transactions/commitPayload.d.ts +15 -0
- package/dist/transactions/commitPayload.js +6 -0
- package/dist/transactions/httpCommitEnvelope.d.ts +43 -0
- package/dist/transactions/httpCommitEnvelope.js +179 -0
- package/dist/transactions/replayValidation.d.ts +83 -0
- package/dist/transactions/replayValidation.js +46 -1
- package/dist/wire/bootstrapReason.d.ts +9 -0
- package/dist/wire/bootstrapReason.js +8 -0
- package/dist/wire/frames.d.ts +236 -0
- package/dist/wire/frames.js +21 -0
- package/dist/wire/index.d.ts +4 -2
- package/dist/wire/index.js +2 -1
- package/docs/api.md +10 -10
- package/docs/coordination.md +2 -2
- package/docs/mcp.md +1 -1
- package/package.json +7 -2
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transport-independent v2 commit contract.
|
|
3
|
+
*
|
|
4
|
+
* A commit is a caller's atomic write intent. It contains ordered operations,
|
|
5
|
+
* a stable idempotency key, and explicit write preconditions. HTTP and
|
|
6
|
+
* WebSocket transports wrap these schemas; they do not redefine their data.
|
|
7
|
+
* Every public data type in this module is inferred from the Zod schema that
|
|
8
|
+
* validates it at a trust boundary.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { ERROR_CODES } from '../errorCodes.js';
|
|
12
|
+
/** Current version of the transport-independent commit contract. */
|
|
13
|
+
export const COMMIT_CONTRACT_VERSION = 2;
|
|
14
|
+
// ── Opaque identifiers ───────────────────────────────────────────────────
|
|
15
|
+
/** Stable identity of one logical commit. Reused unchanged for every retry. */
|
|
16
|
+
export const idempotencyKeySchema = z.string().min(1).max(255).brand();
|
|
17
|
+
/** Server-issued identity of a durable commit execution. */
|
|
18
|
+
export const commitIdSchema = z.uuid().brand();
|
|
19
|
+
/** Stable identity of one operation within a commit. */
|
|
20
|
+
export const operationIdSchema = z.string().min(1).max(255).brand();
|
|
21
|
+
/** Identity of a pessimistic claim whose generation fences a write. */
|
|
22
|
+
export const claimIdSchema = z.string().min(1).max(255).brand();
|
|
23
|
+
/**
|
|
24
|
+
* Internal committed-log offset. Decimal strings preserve PostgreSQL BIGINT
|
|
25
|
+
* precision across JavaScript and JSON boundaries.
|
|
26
|
+
*/
|
|
27
|
+
export const syncOffsetSchema = z
|
|
28
|
+
.string()
|
|
29
|
+
.regex(/^(0|[1-9]\d*)$/, 'Expected a non-negative decimal sync offset')
|
|
30
|
+
.brand();
|
|
31
|
+
/** Opaque resumable cursor returned to clients; consumers must not inspect it. */
|
|
32
|
+
export const syncCursorSchema = z.string().min(1).max(4096).brand();
|
|
33
|
+
// ── JSON values ──────────────────────────────────────────────────────────
|
|
34
|
+
/** JSON-safe value accepted by a persisted or fingerprinted commit payload. */
|
|
35
|
+
export const jsonValueSchema = z.json();
|
|
36
|
+
/** JSON object accepted for entity values, patches, and structured details. */
|
|
37
|
+
export const jsonObjectSchema = z.record(z.string(), jsonValueSchema);
|
|
38
|
+
const nonEmptyJsonObjectSchema = jsonObjectSchema.refine((value) => Object.keys(value).length > 0, 'Expected at least one changed field');
|
|
39
|
+
// ── Intent ───────────────────────────────────────────────────────────────
|
|
40
|
+
/** Model and row addressed by an operation. */
|
|
41
|
+
export const entityRefSchema = z.strictObject({
|
|
42
|
+
model: z.string().min(1).max(255),
|
|
43
|
+
id: z.string().min(1).max(1024),
|
|
44
|
+
});
|
|
45
|
+
const fieldPathSchema = z.string().min(1).max(1024);
|
|
46
|
+
/**
|
|
47
|
+
* A condition that must still hold when an operation is applied. Preconditions
|
|
48
|
+
* are data rather than option flags so they are included in the request
|
|
49
|
+
* fingerprint and can be reported precisely when a commit conflicts.
|
|
50
|
+
*/
|
|
51
|
+
export const writePreconditionSchema = z.discriminatedUnion('type', [
|
|
52
|
+
z.strictObject({
|
|
53
|
+
type: z.literal('unchanged_since'),
|
|
54
|
+
cursor: syncOffsetSchema,
|
|
55
|
+
fields: z.array(fieldPathSchema).min(1).max(256).optional(),
|
|
56
|
+
}),
|
|
57
|
+
z.strictObject({
|
|
58
|
+
type: z.literal('version_matches'),
|
|
59
|
+
version: z.number().int().nonnegative(),
|
|
60
|
+
}),
|
|
61
|
+
z.strictObject({
|
|
62
|
+
type: z.literal('claim_fence'),
|
|
63
|
+
claimId: claimIdSchema,
|
|
64
|
+
generation: z.number().int().positive(),
|
|
65
|
+
}),
|
|
66
|
+
]);
|
|
67
|
+
const operationBaseShape = {
|
|
68
|
+
operationId: operationIdSchema,
|
|
69
|
+
target: entityRefSchema,
|
|
70
|
+
preconditions: z.array(writePreconditionSchema).max(16).default([]),
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* One ordered entity change within a commit. The discriminant makes illegal
|
|
74
|
+
* combinations unrepresentable: only create carries `value`, only patch
|
|
75
|
+
* carries `changes`, and destructive actions carry neither.
|
|
76
|
+
*/
|
|
77
|
+
export const commitOperationSchema = z.discriminatedUnion('type', [
|
|
78
|
+
z.strictObject({
|
|
79
|
+
...operationBaseShape,
|
|
80
|
+
type: z.literal('create'),
|
|
81
|
+
value: jsonObjectSchema,
|
|
82
|
+
}),
|
|
83
|
+
z.strictObject({
|
|
84
|
+
...operationBaseShape,
|
|
85
|
+
type: z.literal('patch'),
|
|
86
|
+
changes: nonEmptyJsonObjectSchema,
|
|
87
|
+
}),
|
|
88
|
+
z.strictObject({
|
|
89
|
+
...operationBaseShape,
|
|
90
|
+
type: z.literal('delete'),
|
|
91
|
+
}),
|
|
92
|
+
z.strictObject({
|
|
93
|
+
...operationBaseShape,
|
|
94
|
+
type: z.literal('archive'),
|
|
95
|
+
}),
|
|
96
|
+
z.strictObject({
|
|
97
|
+
...operationBaseShape,
|
|
98
|
+
type: z.literal('unarchive'),
|
|
99
|
+
}),
|
|
100
|
+
]);
|
|
101
|
+
/** Semantic lineage included in the durable intent and its fingerprint. */
|
|
102
|
+
export const commitMetadataSchema = z.strictObject({
|
|
103
|
+
label: z.string().min(1).max(255).optional(),
|
|
104
|
+
causedByTaskId: z.string().min(1).max(255).optional(),
|
|
105
|
+
});
|
|
106
|
+
const commitIntentShape = {
|
|
107
|
+
schemaVersion: z.literal(COMMIT_CONTRACT_VERSION),
|
|
108
|
+
operations: z.array(commitOperationSchema).min(1).max(500),
|
|
109
|
+
metadata: commitMetadataSchema.optional(),
|
|
110
|
+
};
|
|
111
|
+
/** The portion of a request that is fingerprinted, preserving operation order. */
|
|
112
|
+
export const commitIntentSchema = z.strictObject(commitIntentShape);
|
|
113
|
+
/** A caller's stable idempotency identity plus the intent it identifies. */
|
|
114
|
+
export const commitRequestSchema = z.strictObject({
|
|
115
|
+
...commitIntentShape,
|
|
116
|
+
idempotencyKey: idempotencyKeySchema,
|
|
117
|
+
});
|
|
118
|
+
/** Returns the exact semantic value that an idempotency fingerprint covers. */
|
|
119
|
+
export function commitIntentOf(request) {
|
|
120
|
+
const { idempotencyKey, ...intent } = request;
|
|
121
|
+
void idempotencyKey;
|
|
122
|
+
return intent;
|
|
123
|
+
}
|
|
124
|
+
// ── Result ───────────────────────────────────────────────────────────────
|
|
125
|
+
const registeredWireErrorCodes = Object.entries(ERROR_CODES)
|
|
126
|
+
.filter(([, spec]) => spec.surface === 'wire')
|
|
127
|
+
.map(([code]) => code);
|
|
128
|
+
const policyErrorCodeSchema = z.custom((value) => typeof value === 'string' && /^policy:.+$/.test(value), 'Expected a registered error code or policy:<reason>');
|
|
129
|
+
/** Machine-readable failure code carried by a rejected v2 commit. */
|
|
130
|
+
export const commitErrorCodeSchema = z.union([
|
|
131
|
+
z.enum(registeredWireErrorCodes),
|
|
132
|
+
policyErrorCodeSchema,
|
|
133
|
+
]);
|
|
134
|
+
/** Structured capability a rejected commit would require on retry. */
|
|
135
|
+
export const requiredCapabilitySchema = z.strictObject({
|
|
136
|
+
scope: z.string().min(1),
|
|
137
|
+
constraints: z.record(z.string(), z.union([z.string(), z.array(z.string())])).optional(),
|
|
138
|
+
issuer: z.string().min(1).optional(),
|
|
139
|
+
ttlSeconds: z.number().int().positive().optional(),
|
|
140
|
+
nonce: z.string().min(1).optional(),
|
|
141
|
+
});
|
|
142
|
+
/** Structured terminal error for a commit that did not apply. */
|
|
143
|
+
export const commitErrorSchema = z.strictObject({
|
|
144
|
+
code: commitErrorCodeSchema,
|
|
145
|
+
message: z.string().min(1),
|
|
146
|
+
field: fieldPathSchema.optional(),
|
|
147
|
+
requiredCapability: requiredCapabilitySchema.optional(),
|
|
148
|
+
details: jsonObjectSchema.optional(),
|
|
149
|
+
});
|
|
150
|
+
/** One failed precondition and the smallest current state needed to reconcile. */
|
|
151
|
+
export const commitConflictSchema = z.strictObject({
|
|
152
|
+
operationId: operationIdSchema,
|
|
153
|
+
target: entityRefSchema,
|
|
154
|
+
fields: z.array(fieldPathSchema).max(256),
|
|
155
|
+
currentValues: jsonObjectSchema,
|
|
156
|
+
observedAt: syncOffsetSchema,
|
|
157
|
+
});
|
|
158
|
+
const commitReceiptBaseShape = {
|
|
159
|
+
object: z.literal('commit_receipt'),
|
|
160
|
+
schemaVersion: z.literal(COMMIT_CONTRACT_VERSION),
|
|
161
|
+
idempotencyKey: idempotencyKeySchema,
|
|
162
|
+
commitId: commitIdSchema,
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Terminal result of a commit execution. Expected outcomes are a discriminated
|
|
166
|
+
* union, so committed, conflicted, and rejected results cannot expose fields
|
|
167
|
+
* belonging to another state.
|
|
168
|
+
*/
|
|
169
|
+
export const commitReceiptSchema = z.discriminatedUnion('status', [
|
|
170
|
+
z.strictObject({
|
|
171
|
+
...commitReceiptBaseShape,
|
|
172
|
+
status: z.literal('committed'),
|
|
173
|
+
syncCursor: syncCursorSchema,
|
|
174
|
+
operationCount: z.number().int().min(1).max(500),
|
|
175
|
+
}),
|
|
176
|
+
z.strictObject({
|
|
177
|
+
...commitReceiptBaseShape,
|
|
178
|
+
status: z.literal('conflicted'),
|
|
179
|
+
syncCursor: syncCursorSchema,
|
|
180
|
+
conflicts: z.array(commitConflictSchema).min(1).max(500),
|
|
181
|
+
}),
|
|
182
|
+
z.strictObject({
|
|
183
|
+
...commitReceiptBaseShape,
|
|
184
|
+
status: z.literal('rejected'),
|
|
185
|
+
error: commitErrorSchema,
|
|
186
|
+
}),
|
|
187
|
+
]);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable commit-contract entry point. Import from `@abloatai/ablo/commit`
|
|
3
|
+
* when validating, persisting, or fingerprinting v2 commit data.
|
|
4
|
+
*/
|
|
5
|
+
export { COMMIT_CONTRACT_VERSION, idempotencyKeySchema, commitIdSchema, operationIdSchema, claimIdSchema, syncOffsetSchema, syncCursorSchema, jsonValueSchema, jsonObjectSchema, entityRefSchema, writePreconditionSchema, commitOperationSchema, commitMetadataSchema, commitIntentSchema, commitRequestSchema, commitIntentOf, commitErrorCodeSchema, requiredCapabilitySchema, commitErrorSchema, commitConflictSchema, commitReceiptSchema, } from './contract.js';
|
|
6
|
+
export type { IdempotencyKey, CommitId, OperationId, ClaimId, SyncOffset, SyncCursor, JsonValue, JsonObject, EntityRef, WritePrecondition, CommitOperationInput, CommitOperation, CommitMetadata, CommitIntentInput, CommitIntent, CommitRequestInput, CommitRequest, CommitErrorCode, RequiredCapability, CommitError, CommitConflict, CommitReceiptInput, CommitReceipt, CommittedCommitReceipt, ConflictedCommitReceipt, RejectedCommitReceipt, } from './contract.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable commit-contract entry point. Import from `@abloatai/ablo/commit`
|
|
3
|
+
* when validating, persisting, or fingerprinting v2 commit data.
|
|
4
|
+
*/
|
|
5
|
+
export { COMMIT_CONTRACT_VERSION, idempotencyKeySchema, commitIdSchema, operationIdSchema, claimIdSchema, syncOffsetSchema, syncCursorSchema, jsonValueSchema, jsonObjectSchema, entityRefSchema, writePreconditionSchema, commitOperationSchema, commitMetadataSchema, commitIntentSchema, commitRequestSchema, commitIntentOf, commitErrorCodeSchema, requiredCapabilitySchema, commitErrorSchema, commitConflictSchema, commitReceiptSchema, } from './contract.js';
|
|
@@ -21,6 +21,8 @@ import { LoadStrategy } from '../types/index.js';
|
|
|
21
21
|
*/
|
|
22
22
|
export declare class StoreManager {
|
|
23
23
|
private stores;
|
|
24
|
+
/** Strict-durability wrapper for the client write journal and commit outbox. */
|
|
25
|
+
private transactionStore;
|
|
24
26
|
private syncactionStore;
|
|
25
27
|
private db;
|
|
26
28
|
private isInitialized;
|
|
@@ -22,6 +22,8 @@ import { AbloValidationError } from '../errors.js';
|
|
|
22
22
|
*/
|
|
23
23
|
export class StoreManager {
|
|
24
24
|
stores = new Map();
|
|
25
|
+
/** Strict-durability wrapper for the client write journal and commit outbox. */
|
|
26
|
+
transactionStore = null;
|
|
25
27
|
syncactionStore = null;
|
|
26
28
|
db = null;
|
|
27
29
|
isInitialized = false;
|
|
@@ -46,6 +48,12 @@ export class StoreManager {
|
|
|
46
48
|
for (const modelName of allModels) {
|
|
47
49
|
await this.createStoreForModel(modelName);
|
|
48
50
|
}
|
|
51
|
+
// Special stores are created during the IDB upgrade, but unlike model
|
|
52
|
+
// stores they have no registry metadata and therefore need an explicit
|
|
53
|
+
// runtime wrapper. Outbox acknowledgement must mean disk-backed, so this
|
|
54
|
+
// store opts into strict durability rather than the cache stores' relaxed
|
|
55
|
+
// mode.
|
|
56
|
+
this.transactionStore = new ObjectStore(this.db, '__transactions', '__transactions', { loadStrategy: LoadStrategy.instant }, 'strict');
|
|
49
57
|
// Initialize SyncactionStore
|
|
50
58
|
this.syncactionStore = new SyncActionStore(this.db);
|
|
51
59
|
await this.syncactionStore.initialize();
|
|
@@ -142,6 +150,9 @@ export class StoreManager {
|
|
|
142
150
|
* Get ObjectStore for a model
|
|
143
151
|
*/
|
|
144
152
|
getStore(modelName) {
|
|
153
|
+
if (modelName === '__transactions') {
|
|
154
|
+
return this.transactionStore ?? undefined;
|
|
155
|
+
}
|
|
145
156
|
return this.stores.get(modelName);
|
|
146
157
|
}
|
|
147
158
|
/**
|
|
@@ -295,6 +306,7 @@ export class StoreManager {
|
|
|
295
306
|
for (const store of this.stores.values()) {
|
|
296
307
|
store.markAsClosing();
|
|
297
308
|
}
|
|
309
|
+
this.transactionStore?.markAsClosing();
|
|
298
310
|
// SyncActionStore is a standalone store that does not extend ObjectStore
|
|
299
311
|
// and has no markAsClosing equivalent, so it is intentionally skipped here.
|
|
300
312
|
// If closing-state coordination is ever needed for sync actions, add it
|
package/dist/errorCodes.js
CHANGED
|
@@ -112,7 +112,7 @@ export const ERROR_CODES = {
|
|
|
112
112
|
jwt_invalid: wire('auth', 401, false, "The bearer JWT failed validation for a reason the server could not classify further. Check the token's issuer, signature, audience, and expiry."),
|
|
113
113
|
jwt_malformed: wire('auth', 401, false, 'The bearer token is not a well-formed JWT and could not be decoded. Check that the full, unmodified token was sent.'),
|
|
114
114
|
jwt_missing_issuer: wire('auth', 401, false, 'The bearer JWT has no `iss` (issuer) claim, so it cannot be routed to a trusted issuer.'),
|
|
115
|
-
jwt_issuer_untrusted: wire('auth', 401, false, "The bearer JWT's `iss` is not a registered trusted issuer.
|
|
115
|
+
jwt_issuer_untrusted: wire('auth', 401, false, "The bearer JWT's `iss` is not a registered trusted issuer. Check the token's issuer claim, or register the issuer with your deployment before retrying."),
|
|
116
116
|
jwt_signature_invalid: wire('auth', 401, false, "The bearer JWT's signature could not be verified against the issuer's JWKS (wrong key, rotated key, or forged token)."),
|
|
117
117
|
jwt_audience_mismatch: wire('auth', 401, false, "The bearer JWT's `aud` (audience) claim does not match the audience this issuer is registered with."),
|
|
118
118
|
jwt_missing_subject: wire('auth', 401, false, 'The bearer JWT has no `sub` (subject) claim to identify the user.'),
|
|
@@ -161,7 +161,11 @@ export const ERROR_CODES = {
|
|
|
161
161
|
model_claim_not_configured: client('claim', 'Claiming requires the collaboration runtime, which the standard Ablo({ schema, apiKey }) client wires up for every model automatically — there is no per-model claim configuration to add. This appears only when a model proxy is constructed directly without that runtime (an internal/advanced path).'),
|
|
162
162
|
model_watch_not_configured: client('claim', 'watch() opens a presence/claim subscription and needs a live WebSocket, so it is unavailable on the HTTP transport and on model proxies built without a socket. Use the standard Ablo({ schema, apiKey }) client (default WebSocket transport).'),
|
|
163
163
|
// ── stale context / idempotency (409) ──────────────────────────────
|
|
164
|
-
|
|
164
|
+
// Not retryable at the transport: the rejected request carries its frozen
|
|
165
|
+
// `readAt`, so resending the identical payload can never succeed. Recovery
|
|
166
|
+
// is a caller-level re-read that produces a NEW request with a fresh
|
|
167
|
+
// watermark — the same shape as `claim_conflict`.
|
|
168
|
+
stale_context: wire('conflict', 409, false, "The row changed after you read it — the write's `readAt` watermark is older than the current row version. Re-read the row and retry."),
|
|
165
169
|
// Raised by the functional `update(id, current => next)` form once its
|
|
166
170
|
// internal reconcile budget is exhausted, because the row stayed continuously
|
|
167
171
|
// contended. The SDK has already retried; the caller decides whether to back
|
package/dist/index.d.ts
CHANGED
|
@@ -55,10 +55,16 @@ export type { MutationExecutor } from './interfaces/index.js';
|
|
|
55
55
|
export type { ModelUpdater, ContentionOptions } from './client/functionalUpdate.js';
|
|
56
56
|
export { DEFAULT_CONTENTION_RETRIES } from './client/functionalUpdate.js';
|
|
57
57
|
export type { HttpClaimApi, InternalAbloOptions } from './client/Ablo.js';
|
|
58
|
+
export type { AbloReads } from './client/Ablo.js';
|
|
58
59
|
export { type AbloHttpClientOptions, type AbloHttpClient, type HttpModelClient, } from './client/httpClient.js';
|
|
59
60
|
export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './client/auth.js';
|
|
60
61
|
export type { AbloOptions, LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, ModelOperations, } from './client/Ablo.js';
|
|
61
62
|
export type { AbloPersistence } from './client/persistence.js';
|
|
63
|
+
export type { CommitOutboxRecord, CommitOutboxStore, } from './transactions/commitOutboxStore.js';
|
|
64
|
+
export { durableCommitEnvelopeSchema, } from './transactions/commitEnvelope.js';
|
|
65
|
+
export type { CommitOutboxScope, DurableCommitEnvelope, } from './transactions/commitEnvelope.js';
|
|
66
|
+
export { durableHttpCommitEnvelopeSchema, } from './transactions/httpCommitEnvelope.js';
|
|
67
|
+
export type { DurableHttpCommitEnvelope, } from './transactions/httpCommitEnvelope.js';
|
|
62
68
|
import { Ablo } from './client/Ablo.js';
|
|
63
69
|
export default Ablo;
|
|
64
70
|
export { dataSource, abloSource, sourceEventForOperation, signAbloSourceRequest, verifyAbloSourceRequest, } from './source/index.js';
|
package/dist/index.js
CHANGED
|
@@ -63,6 +63,8 @@ export { DEFAULT_CONTENTION_RETRIES } from './client/functionalUpdate.js';
|
|
|
63
63
|
// `AbloHttpClient` type, which is the return type of that call.
|
|
64
64
|
export {} from './client/httpClient.js';
|
|
65
65
|
export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './client/auth.js';
|
|
66
|
+
export { durableCommitEnvelopeSchema, } from './transactions/commitEnvelope.js';
|
|
67
|
+
export { durableHttpCommitEnvelopeSchema, } from './transactions/httpCommitEnvelope.js';
|
|
66
68
|
// Participant types live under `Ablo.Participant.*` —
|
|
67
69
|
// `Ablo.Participant.Joined`, `Ablo.Participant.Manager`,
|
|
68
70
|
// `Ablo.Participant.JoinOptions`, etc. Same dot-access shape as
|
|
@@ -200,8 +200,8 @@ export interface CommitResult {
|
|
|
200
200
|
* - `idempotencyKey` — when set, the server caches the response for 24 hours and
|
|
201
201
|
* returns the cached result on any retry using the same key. When omitted, the
|
|
202
202
|
* SDK generates a fresh UUID per mutation, so every call is retry-safe by
|
|
203
|
-
* default.
|
|
204
|
-
* write
|
|
203
|
+
* default. `null` is retained for source compatibility and is treated like
|
|
204
|
+
* omission; write retries never opt out of request identity.
|
|
205
205
|
* - `label` — a human-readable tag recorded with the mutation for debugging, such
|
|
206
206
|
* as "nightly cleanup" or "user click".
|
|
207
207
|
*/
|
|
@@ -250,6 +250,8 @@ export declare class UndoManager<S extends Schema> {
|
|
|
250
250
|
private readonly store;
|
|
251
251
|
private readonly organizationId;
|
|
252
252
|
private readonly scopes;
|
|
253
|
+
/** The options each scope was constructed with, for the mismatch warning below. */
|
|
254
|
+
private readonly creationOptions;
|
|
253
255
|
constructor(schema: S, store: SyncStoreContract, organizationId: string);
|
|
254
256
|
getScope(name: string, options?: UndoScopeOptions): UndoScope<S>;
|
|
255
257
|
clearAll(): void;
|
|
@@ -569,6 +569,8 @@ export class UndoManager {
|
|
|
569
569
|
store;
|
|
570
570
|
organizationId;
|
|
571
571
|
scopes = new Map();
|
|
572
|
+
/** The options each scope was constructed with, for the mismatch warning below. */
|
|
573
|
+
creationOptions = new Map();
|
|
572
574
|
constructor(schema, store, organizationId) {
|
|
573
575
|
this.schema = schema;
|
|
574
576
|
this.store = store;
|
|
@@ -579,6 +581,36 @@ export class UndoManager {
|
|
|
579
581
|
if (!scope) {
|
|
580
582
|
scope = new UndoScope(this.schema, this.store, this.organizationId, options);
|
|
581
583
|
this.scopes.set(name, scope);
|
|
584
|
+
this.creationOptions.set(name, options);
|
|
585
|
+
return scope;
|
|
586
|
+
}
|
|
587
|
+
// A scope keeps the options it was created with; later calls cannot change
|
|
588
|
+
// them. Requesting the shared scope with no options is the normal pattern
|
|
589
|
+
// and stays silent — but passing options that conflict with the creation
|
|
590
|
+
// values means one caller believes it configured a scope that another
|
|
591
|
+
// caller already configured differently, which is how a surface silently
|
|
592
|
+
// ends up with, say, no stream recording. Surface that instead of letting
|
|
593
|
+
// it pass.
|
|
594
|
+
if (options) {
|
|
595
|
+
const created = this.creationOptions.get(name);
|
|
596
|
+
const conflicts = [];
|
|
597
|
+
if (options.recordFromStream !== undefined &&
|
|
598
|
+
options.recordFromStream !== (created?.recordFromStream ?? false)) {
|
|
599
|
+
conflicts.push('recordFromStream');
|
|
600
|
+
}
|
|
601
|
+
if (options.maxHistory !== undefined && options.maxHistory !== (created?.maxHistory ?? 100)) {
|
|
602
|
+
conflicts.push('maxHistory');
|
|
603
|
+
}
|
|
604
|
+
if (options.conflictPolicy !== undefined &&
|
|
605
|
+
options.conflictPolicy !== (created?.conflictPolicy ?? DEFAULT_UNDO_CONFLICT_POLICY)) {
|
|
606
|
+
conflicts.push('conflictPolicy');
|
|
607
|
+
}
|
|
608
|
+
if (conflicts.length > 0) {
|
|
609
|
+
getContext().logger.warn(`The undo scope "${name}" already exists with different options — ` +
|
|
610
|
+
`${conflicts.join(', ')} cannot be changed after creation and the requested ` +
|
|
611
|
+
`values are ignored. Create the scope with its full options before any ` +
|
|
612
|
+
`caller requests it without them, or use a differently named scope.`);
|
|
613
|
+
}
|
|
582
614
|
}
|
|
583
615
|
return scope;
|
|
584
616
|
}
|
package/dist/react/useAblo.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Ablo, ModelClaim } from '../client/Ablo.js';
|
|
1
|
+
import type { Ablo, AbloReads, ModelClaim } from '../client/Ablo.js';
|
|
2
2
|
import type { ModelOperations } from '../client/createModelProxy.js';
|
|
3
3
|
import type { SchemaRecord } from '../schema/schema.js';
|
|
4
4
|
import type { ResolveSchema } from '../types/global.js';
|
|
@@ -11,8 +11,8 @@ import type { ResolveSchema } from '../types/global.js';
|
|
|
11
11
|
type DefaultModels = ResolveSchema extends {
|
|
12
12
|
models: infer M;
|
|
13
13
|
} ? M extends SchemaRecord ? M : SchemaRecord : SchemaRecord;
|
|
14
|
-
type ModelClientSelector<R extends SchemaRecord, T, C> = (ablo:
|
|
15
|
-
type AbloSelector<R extends SchemaRecord, T> = (ablo:
|
|
14
|
+
type ModelClientSelector<R extends SchemaRecord, T, C> = (ablo: AbloReads<R>) => ModelOperations<T, C>;
|
|
15
|
+
type AbloSelector<R extends SchemaRecord, T> = (ablo: AbloReads<R>) => T;
|
|
16
16
|
export interface UseAbloModelOptions<T> {
|
|
17
17
|
/**
|
|
18
18
|
* An initial row, usually from a server component or a route loader. The hook
|
|
@@ -48,7 +48,9 @@ export type UseAbloHydratedModelResult<T> = Omit<UseAbloModelResult<T>, 'data'>
|
|
|
48
48
|
* if (!ablo) return <Loading />;
|
|
49
49
|
* const doc = await ablo.documents.retrieve({ id }); // async server read
|
|
50
50
|
*
|
|
51
|
-
* // Reactive selector (a synchronous local snapshot)
|
|
51
|
+
* // Reactive selector (a synchronous local snapshot). The selector's reads
|
|
52
|
+
* // are typed as snapshot rows — data fields + computeds, no relation
|
|
53
|
+
* // accessors — matching what the hook actually returns:
|
|
52
54
|
* const doc = useAblo((ablo) => ablo.documents.get(id)) ?? serverDoc;
|
|
53
55
|
* const active = useAblo((ablo) => ablo.documents.claim.state({ id }));
|
|
54
56
|
*
|
package/dist/react/useAblo.js
CHANGED
|
@@ -5,6 +5,24 @@ import { getModelClientMeta } from '../client/createModelProxy.js';
|
|
|
5
5
|
import { Model } from '../Model.js';
|
|
6
6
|
import { useReactive } from './useReactive.js';
|
|
7
7
|
const EMPTY_CLAIMS = Object.freeze([]);
|
|
8
|
+
/**
|
|
9
|
+
* Restore the caller's schema generics on the context-held engine. React
|
|
10
|
+
* context erases generics (see `AbloInternalContextValue.engine`), so this is
|
|
11
|
+
* the one deliberate rebind point: the runtime value is the fully typed
|
|
12
|
+
* client, and `R` is the compile-time view the calling hook declared.
|
|
13
|
+
*/
|
|
14
|
+
function rebindEngine(engine) {
|
|
15
|
+
return engine;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The same rebind viewed through the reactive-read surface — the identical
|
|
19
|
+
* runtime object, with model reads typed as snapshot rows, because everything
|
|
20
|
+
* a selector returns is converted through `snapshotValue` before the hook
|
|
21
|
+
* hands it back.
|
|
22
|
+
*/
|
|
23
|
+
function reactiveReads(engine) {
|
|
24
|
+
return rebindEngine(engine);
|
|
25
|
+
}
|
|
8
26
|
function readModelResult(engine, modelClient, id, initial) {
|
|
9
27
|
if (!modelClient || id === undefined) {
|
|
10
28
|
return { data: initial, claims: EMPTY_CLAIMS, claimed: false };
|
|
@@ -43,7 +61,7 @@ export function useAblo(modelOrSelect, id, options) {
|
|
|
43
61
|
const isSelectorOnly = typeof modelOrSelect === 'function' && id === undefined;
|
|
44
62
|
const modelClient = typeof modelOrSelect === 'function' && id !== undefined
|
|
45
63
|
? engine
|
|
46
|
-
? modelOrSelect(engine)
|
|
64
|
+
? modelOrSelect(reactiveReads(engine))
|
|
47
65
|
: undefined
|
|
48
66
|
: typeof modelOrSelect === 'function'
|
|
49
67
|
? undefined
|
|
@@ -66,7 +84,11 @@ export function useAblo(modelOrSelect, id, options) {
|
|
|
66
84
|
if (!engine || !isSelectorOnly || typeof modelOrSelect !== 'function') {
|
|
67
85
|
return undefined;
|
|
68
86
|
}
|
|
69
|
-
return
|
|
87
|
+
// The selector runs against the real engine — reads inside it return the
|
|
88
|
+
// pool's model instances. `snapshotValue` then converts the RESULT to
|
|
89
|
+
// plain snapshot rows, which is what the selector's `AbloReads`
|
|
90
|
+
// parameter type already promised.
|
|
91
|
+
return snapshotValue(modelOrSelect(reactiveReads(engine)));
|
|
70
92
|
});
|
|
71
93
|
const modelResult = useReactive(() => {
|
|
72
94
|
void claimVersion;
|
|
@@ -76,5 +98,5 @@ export function useAblo(modelOrSelect, id, options) {
|
|
|
76
98
|
return selected;
|
|
77
99
|
if (modelOrSelect)
|
|
78
100
|
return modelResult;
|
|
79
|
-
return engine;
|
|
101
|
+
return engine === null ? null : rebindEngine(engine);
|
|
80
102
|
}
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export { syncDeltaActionSchema, wireDeltaDataSchema, participantRefSchema, syncD
|
|
|
30
30
|
export { model, scopeKindOf, type ModelDef, type ModelOptions, type LoadStrategy, type PersistOptions, type RelationRecord, type GrantsRef, type ConflictAxis, } from './model.js';
|
|
31
31
|
export { coordination, humansOverwrite, humansReject, humansNotify, agentsOverwrite, agentsReject, agentsNotify, systemOverwrite, systemReject, systemNotify, type ConflictRule, } from './coordination.js';
|
|
32
32
|
export { mutable, readOnly, type SugarOptions } from './sugar.js';
|
|
33
|
-
export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type Model, type InferModel, type InferCreate, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, composeEntitySyncGroups, intersectRequestedWithAllowed, type IdentityRole, type IdentityContext, type IdentityRoleSource, type EntityRole, type EntityContext, type EntityRoleSource, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, } from './schema.js';
|
|
33
|
+
export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type Model, type Row, type InferModel, type InferCreate, type InferRow, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, composeEntitySyncGroups, intersectRequestedWithAllowed, type IdentityRole, type IdentityContext, type IdentityRoleSource, type EntityRole, type EntityContext, type EntityRoleSource, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, } from './schema.js';
|
|
34
34
|
export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, type SchemaJSON, type ModelJSON, type RelationJSON, } from './serialize.js';
|
|
35
35
|
export { selectModels } from './select.js';
|
|
36
36
|
export { generateProvisionPlan, generateMigrationPlan, appSchemaName, camelToSnake, snakeToCamel, q, sqlType, type ProvisionPlan, type MigrationPlan, } from './ddl.js';
|
package/dist/schema/schema.d.ts
CHANGED
|
@@ -162,8 +162,38 @@ Omit<z.infer<z.ZodObject<Shape>>, keyof BaseModelFields> & BaseModelFields & Bas
|
|
|
162
162
|
* to `slide.layers` would have no effect at runtime.
|
|
163
163
|
*/
|
|
164
164
|
export type InferRelations<S extends Schema, R extends RelationRecord> = string extends keyof R ? unknown : {
|
|
165
|
-
readonly [K in keyof R]: R[K] extends RelationDef<infer Type, infer Target
|
|
165
|
+
readonly [K in keyof R]: R[K] extends RelationDef<infer Type, infer Target> ? Target extends keyof S['models'] ? Type extends 'hasMany' ? InferModel<S, Target>[] : Type extends 'hasOne' | 'belongsTo' ? InferModel<S, Target> | undefined : never : never : never;
|
|
166
166
|
};
|
|
167
|
+
/**
|
|
168
|
+
* The row shape a reactive read returns: the model's data fields, base fields,
|
|
169
|
+
* and schema computed getters — WITHOUT relation accessors and WITHOUT model
|
|
170
|
+
* methods. Derived from the model definition, exactly like {@link InferModel},
|
|
171
|
+
* mirroring what `toReactiveSnapshot()` produces at runtime.
|
|
172
|
+
*
|
|
173
|
+
* Relations (`hasMany` / `belongsTo`) are store-backed getters that exist only
|
|
174
|
+
* on the pool's model instances, so a reactive row honestly omits them —
|
|
175
|
+
* reading `row.layers` is a compile error instead of a silent `undefined`.
|
|
176
|
+
* Compose relations through a selector or hook instead.
|
|
177
|
+
*
|
|
178
|
+
* The same pairing other data layers converged on: Zero's data-only `Row<...>`
|
|
179
|
+
* with relations added per-query, Prisma's scalar-only model types with
|
|
180
|
+
* `GetPayload<{ include }>`, mobx-state-tree's `Instance<T>` / `SnapshotOut<T>`.
|
|
181
|
+
*/
|
|
182
|
+
export type InferRow<S extends Schema, ModelName extends keyof S['models']> = S['models'][ModelName] extends ModelDef<infer Shape, RelationRecord, infer C> ? // Same reserved-field guard as InferModel — see the comment there.
|
|
183
|
+
Omit<z.infer<z.ZodObject<Shape>>, keyof BaseModelFields> & BaseModelFields & InferComputed<C> : never;
|
|
184
|
+
/**
|
|
185
|
+
* The reactive-row companion to {@link Model}. Once your project's
|
|
186
|
+
* `ablo/register.ts` registers the schema, a single argument is all it takes:
|
|
187
|
+
*
|
|
188
|
+
* ```ts
|
|
189
|
+
* type SlideRow = Row<'slides'>; // data fields + computeds, no relations
|
|
190
|
+
* type Slide = Model<'slides'>; // the pool's model instance
|
|
191
|
+
* ```
|
|
192
|
+
*
|
|
193
|
+
* Without that registration, or for a second schema, pass the schema
|
|
194
|
+
* explicitly: `Row<typeof schema, 'slides'>`.
|
|
195
|
+
*/
|
|
196
|
+
export type Row<A, B = never> = [B] extends [never] ? A extends keyof RegisteredSchema['models'] ? InferRow<RegisteredSchema, A> : never : A extends Schema ? InferRow<A, B extends keyof A['models'] ? B : never> : never;
|
|
167
197
|
/**
|
|
168
198
|
* Infer the return types of computed getters.
|
|
169
199
|
* Maps each computed function's return type into a readonly property.
|
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { ModelMetadata } from '../types/index.js';
|
|
6
6
|
import type { ObjectStoreContract } from './ObjectStoreContract.js';
|
|
7
|
+
/**
|
|
8
|
+
* IDB transaction options type (TypeScript's lib.dom.d.ts may be outdated)
|
|
9
|
+
* durability: 'relaxed' provides ~16x write performance improvement
|
|
10
|
+
* by not requiring fsync on each transaction commit.
|
|
11
|
+
* Safe for optimistic sync engines that can recover from server state.
|
|
12
|
+
*/
|
|
13
|
+
interface IDBTransactionOptionsWithDurability {
|
|
14
|
+
durability?: 'default' | 'relaxed' | 'strict';
|
|
15
|
+
}
|
|
7
16
|
/**
|
|
8
17
|
* An IndexedDB-backed store holding the records of a single model.
|
|
9
18
|
*
|
|
@@ -21,8 +30,11 @@ export declare class ObjectStore implements ObjectStoreContract {
|
|
|
21
30
|
protected modelName: string;
|
|
22
31
|
protected storeName: string;
|
|
23
32
|
protected metadata: ModelMetadata;
|
|
33
|
+
private readonly writeDurability;
|
|
24
34
|
private isClosing;
|
|
25
|
-
constructor(db: IDBDatabase, modelName: string, storeName: string, metadata: ModelMetadata);
|
|
35
|
+
constructor(db: IDBDatabase, modelName: string, storeName: string, metadata: ModelMetadata, writeDurability?: NonNullable<IDBTransactionOptionsWithDurability['durability']>);
|
|
36
|
+
/** Insert without replacing an existing key (used by the commit outbox). */
|
|
37
|
+
add(data: Record<string, unknown>): Promise<void>;
|
|
26
38
|
/**
|
|
27
39
|
* Mark this store as closing to prevent new operations
|
|
28
40
|
*/
|
|
@@ -100,3 +112,4 @@ export declare class ObjectStore implements ObjectStoreContract {
|
|
|
100
112
|
*/
|
|
101
113
|
performMaintenance(): Promise<void>;
|
|
102
114
|
}
|
|
115
|
+
export {};
|
|
@@ -19,12 +19,35 @@ export class ObjectStore {
|
|
|
19
19
|
modelName;
|
|
20
20
|
storeName;
|
|
21
21
|
metadata;
|
|
22
|
+
writeDurability;
|
|
22
23
|
isClosing = false;
|
|
23
|
-
constructor(db, modelName, storeName, metadata) {
|
|
24
|
+
constructor(db, modelName, storeName, metadata, writeDurability = 'relaxed') {
|
|
24
25
|
this.db = db;
|
|
25
26
|
this.modelName = modelName;
|
|
26
27
|
this.storeName = storeName;
|
|
27
28
|
this.metadata = metadata;
|
|
29
|
+
this.writeDurability = writeDurability;
|
|
30
|
+
}
|
|
31
|
+
/** Insert without replacing an existing key (used by the commit outbox). */
|
|
32
|
+
async add(data) {
|
|
33
|
+
if (!this.checkDatabaseAvailable()) {
|
|
34
|
+
return Promise.reject(new Error('IndexedDB not available (closing or invalid)'));
|
|
35
|
+
}
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
try {
|
|
38
|
+
const tx = this.db.transaction([this.storeName], 'readwrite', {
|
|
39
|
+
durability: this.writeDurability,
|
|
40
|
+
});
|
|
41
|
+
const store = tx.objectStore(this.storeName);
|
|
42
|
+
const request = store.add(data);
|
|
43
|
+
tx.oncomplete = () => { resolve(); };
|
|
44
|
+
tx.onerror = () => { reject(tx.error || new Error('IndexedDB transaction error')); };
|
|
45
|
+
request.onerror = () => { reject(request.error || new Error('IndexedDB request error')); };
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
28
51
|
}
|
|
29
52
|
/**
|
|
30
53
|
* Mark this store as closing to prevent new operations
|
|
@@ -65,7 +88,7 @@ export class ObjectStore {
|
|
|
65
88
|
try {
|
|
66
89
|
// Use relaxed durability for ~16x write performance (safe with optimistic sync)
|
|
67
90
|
const tx = this.db.transaction([this.storeName], 'readwrite', {
|
|
68
|
-
durability:
|
|
91
|
+
durability: this.writeDurability,
|
|
69
92
|
});
|
|
70
93
|
const store = tx.objectStore(this.storeName);
|
|
71
94
|
const request = store.put(data);
|
|
@@ -169,7 +192,7 @@ export class ObjectStore {
|
|
|
169
192
|
try {
|
|
170
193
|
// Use relaxed durability for ~16x write performance (safe with optimistic sync)
|
|
171
194
|
const tx = this.db.transaction([this.storeName], 'readwrite', {
|
|
172
|
-
durability:
|
|
195
|
+
durability: this.writeDurability,
|
|
173
196
|
});
|
|
174
197
|
const store = tx.objectStore(this.storeName);
|
|
175
198
|
const request = store.delete(id);
|
|
@@ -205,7 +228,7 @@ export class ObjectStore {
|
|
|
205
228
|
try {
|
|
206
229
|
// Use relaxed durability for ~16x write performance (safe with optimistic sync)
|
|
207
230
|
const tx = this.db.transaction([this.storeName], 'readwrite', {
|
|
208
|
-
durability:
|
|
231
|
+
durability: this.writeDurability,
|
|
209
232
|
});
|
|
210
233
|
const store = tx.objectStore(this.storeName);
|
|
211
234
|
const request = store.clear();
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* a silent failure in a caller.
|
|
16
16
|
*/
|
|
17
17
|
export interface ObjectStoreContract {
|
|
18
|
+
/** Insert a record only when its key is absent. */
|
|
19
|
+
add(data: Record<string, unknown>): Promise<void>;
|
|
18
20
|
/** Insert or update a record. The record must carry an `id` field. */
|
|
19
21
|
put(data: Record<string, unknown>): Promise<void>;
|
|
20
22
|
/** Look up a record by id. */
|
package/dist/surface.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export declare const PUBLIC_LIST_OPTION_KEYS: readonly ["where", "filter", "orde
|
|
|
30
30
|
* The keys of the client constructor options, {@link AbloOptions}. Only
|
|
31
31
|
* `schema` is required; every other key is optional.
|
|
32
32
|
*/
|
|
33
|
-
export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ["schema", "apiKey", "authEndpoint", "databaseUrl", "persistence", "transport", "debug", "logLevel", "authToken", "baseURL", "fetch", "defaultHeaders", "defaultQuery", "dangerouslyAllowBrowser"];
|
|
33
|
+
export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ["schema", "apiKey", "authEndpoint", "databaseUrl", "persistence", "commitOutbox", "commitOutboxScope", "transport", "debug", "logLevel", "authToken", "baseURL", "fetch", "defaultHeaders", "defaultQuery", "dangerouslyAllowBrowser"];
|
|
34
34
|
export type ModelVerb = (typeof PUBLIC_MODEL_VERBS)[number];
|
|
35
35
|
export type ListOptionKey = (typeof PUBLIC_LIST_OPTION_KEYS)[number];
|
|
36
36
|
export type AbloOptionKey = (typeof PUBLIC_ABLO_OPTION_KEYS)[number];
|