@absolutejs/sync 1.8.1 → 1.9.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.
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Sync packs — Convex Components without the lock-in. A pack bundles
3
+ * schemas + collections + mutations + scheduled + permissions + readers/
4
+ * writers + CRDT field declarations into one npm-distributable module
5
+ * registered with a single {@link SyncEngine.registerPack} call.
6
+ *
7
+ * See `syncPacks.design.md` for the rationale and the worked examples.
8
+ *
9
+ * Pack design rules (enforced at register time):
10
+ *
11
+ * - A pack declares which tables it `ownsTables`. Two registered packs
12
+ * cannot claim the same table; the host app's directly-registered
13
+ * tables are NOT counted (host registrations always win).
14
+ * - Name construction (namespacing) is the pack's job — each published
15
+ * pack ships as a factory `create<Name>Pack(config)` that builds names
16
+ * from a `tablePrefix` and the host's user/auth context.
17
+ * - Packs compose via the subscription layer (read each other's
18
+ * collections), never via cross-pack `runMutation` calls.
19
+ */
20
+ import type { CollectionDefinition, JoinCollectionDefinition } from './collection';
21
+ import type { GraphCollectionDefinition } from './graph';
22
+ import type { MutationDefinition, TableWriter } from './mutation';
23
+ import type { PermissionsDefinition } from './permissions';
24
+ import type { ReactiveQueryDefinition, TableReader } from './reactive';
25
+ import type { ScheduleDefinition } from './schedule';
26
+ import type { SchemaDefinition } from './schema';
27
+ import type { SearchCollectionDefinition } from './search';
28
+ import type { CrdtMergeable } from '../crdt';
29
+ /**
30
+ * Same shape as the engine's per-table CRDT field map (see
31
+ * {@link SyncEngine.registerCrdt}). A pack declares CRDT field types
32
+ * here; the engine wires them on registration.
33
+ */
34
+ export type CrdtFieldsMap = Record<string, Record<string, CrdtMergeable<unknown>>>;
35
+ /**
36
+ * A pack — a self-contained bundle of every registration the engine
37
+ * already accepts. The engine's `registerPack(pack)` dispatches each
38
+ * field to the matching `engine.register*` method. There is no new
39
+ * persistence path; packs are pure composition.
40
+ */
41
+ export type SyncPack = {
42
+ /**
43
+ * Pack identifier. Used for devtools labelling and conflict
44
+ * diagnostics. Should match the npm package name (e.g.
45
+ * "@absolutejs/sync-pack-presence").
46
+ */
47
+ name: string;
48
+ /**
49
+ * Pack semver. Surfaced in {@link EngineInspection.packs} and in
50
+ * conflict diagnostics (e.g. "table 'comments' is owned by
51
+ * sync-pack-comments@2.1.0").
52
+ */
53
+ version: string;
54
+ /**
55
+ * Tables this pack OWNS. The engine rejects another pack that also
56
+ * claims one of these. Direct host registrations (e.g.
57
+ * `engine.registerSchema("foo", ...)`) are NOT tracked as ownership,
58
+ * so the host can still extend a pack's table or override its
59
+ * schema/permissions.
60
+ */
61
+ ownsTables: string[];
62
+ /**
63
+ * Tables this pack reads but does NOT own (e.g. a comments pack
64
+ * reads the host's `users` table for author info). Reported in
65
+ * {@link EngineInspection.packs}; not enforced unless
66
+ * {@link requireDependencies} is `true`.
67
+ */
68
+ readsTables?: string[];
69
+ /**
70
+ * When `true`, the engine throws {@link PackMissingDependencyError}
71
+ * at register time if any table in `readsTables` has no registered
72
+ * reader. Default `false` — host-app reads can be wired lazily.
73
+ */
74
+ requireDependencies?: boolean;
75
+ schemas?: SchemaDefinition;
76
+ permissions?: PermissionsDefinition<any>;
77
+ readers?: Record<string, TableReader<any>>;
78
+ writers?: Record<string, TableWriter<any, any, any>>;
79
+ crdt?: CrdtFieldsMap;
80
+ collections?: CollectionDefinition<any, any, any>[];
81
+ joinCollections?: JoinCollectionDefinition<any, any, any, any, any>[];
82
+ graphCollections?: GraphCollectionDefinition<any, any, any>[];
83
+ searchCollections?: SearchCollectionDefinition<any, any, any>[];
84
+ reactiveQueries?: ReactiveQueryDefinition<any, any, any>[];
85
+ mutations?: MutationDefinition<any, any, any>[];
86
+ schedules?: ScheduleDefinition[];
87
+ };
88
+ /**
89
+ * Pack metadata stored on the engine and surfaced via
90
+ * {@link EngineInspection.packs}.
91
+ */
92
+ export type RegisteredPack = {
93
+ name: string;
94
+ version: string;
95
+ ownsTables: string[];
96
+ readsTables: string[];
97
+ };
98
+ /**
99
+ * Thrown by {@link SyncEngine.registerPack} when a pack claims a table
100
+ * that another registered pack already owns. The message names both
101
+ * packs and the colliding table so the operator can pick a
102
+ * `tablePrefix`.
103
+ */
104
+ export declare class PackTableConflictError extends Error {
105
+ readonly table: string;
106
+ readonly existingPack: string;
107
+ readonly newPack: string;
108
+ constructor(table: string, existingPack: string, newPack: string);
109
+ }
110
+ /**
111
+ * Thrown by {@link SyncEngine.registerPack} when a pack has
112
+ * `requireDependencies: true` and at least one table in
113
+ * {@link SyncPack.readsTables} has no registered reader at register
114
+ * time. Pack authors opt into this when their pack cannot function
115
+ * without the host having wired the dependency up front.
116
+ */
117
+ export declare class PackMissingDependencyError extends Error {
118
+ readonly pack: string;
119
+ readonly missingTable: string;
120
+ constructor(pack: string, missingTable: string);
121
+ }
122
+ /**
123
+ * Identity helper. A pack is plain data — the helper exists for type
124
+ * inference, not for runtime behavior.
125
+ *
126
+ * @example
127
+ * export const createPresencePack = (config: PresencePackConfig) =>
128
+ * defineSyncPack({
129
+ * name: '@absolutejs/sync-pack-presence',
130
+ * version: '0.1.0',
131
+ * ownsTables: [resolveTableName('presence', config.tablePrefix)],
132
+ * schemas: { ... },
133
+ * collections: [ ... ],
134
+ * mutations: [ ... ],
135
+ * schedules: [ ... ],
136
+ * });
137
+ */
138
+ export declare const defineSyncPack: (pack: SyncPack) => SyncPack;
@@ -1,5 +1,6 @@
1
1
  import type { MutationActions } from './mutation';
2
2
  import type { ReadHandle } from './reactive';
3
+ import type { RetryPolicy } from './retry';
3
4
  /**
4
5
  * Scheduled functions — server-triggered work whose effects flow through the
5
6
  * change feed, so what a schedule writes goes live to subscribers with no extra
@@ -30,6 +31,19 @@ export type ScheduleDefinition = {
30
31
  pattern: string;
31
32
  /** The work to run on each fire. Writes via `ctx.actions` go live. */
32
33
  run: (ctx: ScheduleContext) => Promise<void> | void;
34
+ /**
35
+ * Opt-in retry of the whole handler on classified-as-retryable errors —
36
+ * same shape and defaults as {@link MutationDefinition.retry}. When set
37
+ * and `run` throws a retryable error, the engine discards the buffered
38
+ * changes, awaits a backoff, and re-runs the handler with a fresh
39
+ * transaction. The handler MUST be idempotent under retry (external
40
+ * side effects fire more than once).
41
+ *
42
+ * For per-item retry (e.g. one of many emails failing), write that
43
+ * loop inside the handler — this outer retry covers transient
44
+ * infrastructure failures of the whole fire, not per-item logic.
45
+ */
46
+ retry?: RetryPolicy;
33
47
  };
34
48
  /**
35
49
  * Define a scheduled function. Identity at runtime (for type inference). Register
@@ -4,6 +4,7 @@ import type { MutationDefinition, TableWriter, TransactionRunner } from './mutat
4
4
  import type { ReactiveQueryDefinition, TableReader } from './reactive';
5
5
  import { type BridgeFetchConfig, type HandlerMetricsHook } from './sandbox';
6
6
  import type { PermissionsDefinition, TablePermissions } from './permissions';
7
+ import type { SyncPack } from './pack';
7
8
  import type { SearchCollectionDefinition } from './search';
8
9
  import type { ScheduleDefinition } from './schedule';
9
10
  import type { EngineActivity, EngineInspection } from './devtools';
@@ -213,6 +214,18 @@ export type SyncEngine = {
213
214
  * }
214
215
  */
215
216
  streamChanges: (options?: StreamChangesOptions) => AsyncIterable<LoggedChange>;
217
+ /**
218
+ * Register a {@link SyncPack} — a self-contained bundle of schemas,
219
+ * permissions, readers/writers, collections, mutations, and schedules.
220
+ * Dispatches each field to the matching `register*` method. Rejects
221
+ * with {@link PackTableConflictError} if the pack claims a table
222
+ * another registered pack already owns; with
223
+ * {@link PackMissingDependencyError} if `requireDependencies` is set
224
+ * and a `readsTables` entry has no registered reader.
225
+ *
226
+ * See `syncPacks.design.md` for the rationale.
227
+ */
228
+ registerPack: (pack: SyncPack) => void;
216
229
  };
217
230
  /**
218
231
  * A single committed change as it appears in the engine's change log and on
package/dist/index.js CHANGED
@@ -935,6 +935,32 @@ var fireMetrics = (hook, record) => {
935
935
  }
936
936
  };
937
937
 
938
+ // src/engine/pack.ts
939
+ class PackTableConflictError extends Error {
940
+ table;
941
+ existingPack;
942
+ newPack;
943
+ constructor(table, existingPack, newPack) {
944
+ super(`Pack "${newPack}" claims table "${table}", but "${existingPack}" already owns it. Use a tablePrefix on one of them.`);
945
+ this.name = "PackTableConflictError";
946
+ this.table = table;
947
+ this.existingPack = existingPack;
948
+ this.newPack = newPack;
949
+ }
950
+ }
951
+
952
+ class PackMissingDependencyError extends Error {
953
+ pack;
954
+ missingTable;
955
+ constructor(pack, missingTable) {
956
+ super(`Pack "${pack}" requires a reader for table "${missingTable}" but none is registered. Call engine.registerReader("${missingTable}", ...) before engine.registerPack.`);
957
+ this.name = "PackMissingDependencyError";
958
+ this.pack = pack;
959
+ this.missingTable = missingTable;
960
+ }
961
+ }
962
+ var defineSyncPack = (pack) => pack;
963
+
938
964
  // src/engine/search.ts
939
965
  var SEARCH_SCORE_FIELD = "_score";
940
966
  var defineSearchCollection = (definition) => ({
@@ -1044,6 +1070,8 @@ var createSyncEngine = (options = {}) => {
1044
1070
  const writers = new Map;
1045
1071
  const readers = new Map;
1046
1072
  const schedules = new Map;
1073
+ const packTableOwners = new Map;
1074
+ const registeredPacks = [];
1047
1075
  const permissions = new Map;
1048
1076
  for (const [table, rules] of Object.entries(options.permissions ?? {})) {
1049
1077
  permissions.set(table, rules);
@@ -1775,7 +1803,7 @@ var createSyncEngine = (options = {}) => {
1775
1803
  }
1776
1804
  };
1777
1805
  };
1778
- return {
1806
+ const engine = {
1779
1807
  register: (collection) => {
1780
1808
  registry.set(collection.name, collection);
1781
1809
  for (const table of collection.tables ?? [collection.name]) {
@@ -2050,13 +2078,136 @@ var createSyncEngine = (options = {}) => {
2050
2078
  throw new Error(`Unknown schedule "${name}"`);
2051
2079
  }
2052
2080
  const runHandler = async (tx) => {
2053
- const { actions, buffered: buffered2 } = makeActions(tx, {}, false);
2081
+ const { actions, buffered } = makeActions(tx, {}, false);
2054
2082
  const db = makeReadHandle({}, new Set, new Set, [], false);
2055
2083
  await schedule.run({ actions, db });
2056
- return buffered2;
2084
+ return buffered;
2057
2085
  };
2058
- const buffered = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
2059
- await applyChangeBatch(buffered);
2086
+ const retry = schedule.retry;
2087
+ const maxAttempts = retry === undefined ? 1 : retry.maxAttempts ?? 5;
2088
+ const isRetryable = retry?.isRetryable ?? isSerializationFailure;
2089
+ const computeDelay = retry?.backoff ?? exponentialBackoff();
2090
+ const maxElapsedMs = retry?.maxElapsedMs ?? 30000;
2091
+ const startedAt = Date.now();
2092
+ let lastError;
2093
+ let attemptsMade = 0;
2094
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2095
+ attemptsMade = attempt;
2096
+ try {
2097
+ const buffered = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
2098
+ await applyChangeBatch(buffered);
2099
+ emitActivity({
2100
+ type: "schedule",
2101
+ at: Date.now(),
2102
+ name,
2103
+ status: "ok"
2104
+ });
2105
+ return;
2106
+ } catch (error) {
2107
+ lastError = error;
2108
+ const elapsedMs = Date.now() - startedAt;
2109
+ const canRetry = attempt < maxAttempts && isRetryable(error) && elapsedMs < maxElapsedMs;
2110
+ if (!canRetry)
2111
+ break;
2112
+ const rawDelay = computeDelay(attempt);
2113
+ const remaining = maxElapsedMs - elapsedMs;
2114
+ if (remaining <= 0)
2115
+ break;
2116
+ const delayMs = Math.max(0, Math.min(rawDelay, remaining));
2117
+ emitActivity({
2118
+ type: "scheduleRetry",
2119
+ at: Date.now(),
2120
+ name,
2121
+ attempt,
2122
+ delayMs,
2123
+ errorName: error instanceof Error ? error.name : "Error",
2124
+ errorMessage: error instanceof Error ? error.message : String(error)
2125
+ });
2126
+ if (delayMs > 0) {
2127
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
2128
+ }
2129
+ }
2130
+ }
2131
+ emitActivity({
2132
+ type: "schedule",
2133
+ at: Date.now(),
2134
+ name,
2135
+ status: "error"
2136
+ });
2137
+ if (attemptsMade > 1) {
2138
+ throw new RetriesExhaustedError(attemptsMade, Date.now() - startedAt, lastError);
2139
+ }
2140
+ throw lastError;
2141
+ },
2142
+ registerPack: (pack) => {
2143
+ for (const table of pack.ownsTables) {
2144
+ const existing = packTableOwners.get(table);
2145
+ if (existing !== undefined) {
2146
+ throw new PackTableConflictError(table, existing, pack.name);
2147
+ }
2148
+ }
2149
+ if (pack.requireDependencies === true) {
2150
+ for (const table of pack.readsTables ?? []) {
2151
+ if (!readers.has(table)) {
2152
+ throw new PackMissingDependencyError(pack.name, table);
2153
+ }
2154
+ }
2155
+ }
2156
+ if (pack.schemas !== undefined) {
2157
+ for (const [table, schema] of Object.entries(pack.schemas)) {
2158
+ engine.registerSchema(table, schema);
2159
+ }
2160
+ }
2161
+ if (pack.permissions !== undefined) {
2162
+ for (const [table, rules] of Object.entries(pack.permissions)) {
2163
+ engine.registerPermissions(table, rules);
2164
+ }
2165
+ }
2166
+ if (pack.readers !== undefined) {
2167
+ for (const [table, reader] of Object.entries(pack.readers)) {
2168
+ engine.registerReader(table, reader);
2169
+ }
2170
+ }
2171
+ if (pack.writers !== undefined) {
2172
+ for (const [table, writer] of Object.entries(pack.writers)) {
2173
+ engine.registerWriter(table, writer);
2174
+ }
2175
+ }
2176
+ if (pack.crdt !== undefined) {
2177
+ for (const [table, fields] of Object.entries(pack.crdt)) {
2178
+ engine.registerCrdt(table, fields);
2179
+ }
2180
+ }
2181
+ for (const collection of pack.collections ?? []) {
2182
+ engine.register(collection);
2183
+ }
2184
+ for (const collection of pack.joinCollections ?? []) {
2185
+ engine.registerJoin(collection);
2186
+ }
2187
+ for (const collection of pack.graphCollections ?? []) {
2188
+ engine.registerGraph(collection);
2189
+ }
2190
+ for (const collection of pack.searchCollections ?? []) {
2191
+ engine.registerSearch(collection);
2192
+ }
2193
+ for (const query of pack.reactiveQueries ?? []) {
2194
+ engine.registerReactive(query);
2195
+ }
2196
+ for (const mutation of pack.mutations ?? []) {
2197
+ engine.registerMutation(mutation);
2198
+ }
2199
+ for (const schedule of pack.schedules ?? []) {
2200
+ engine.registerSchedule(schedule);
2201
+ }
2202
+ for (const table of pack.ownsTables) {
2203
+ packTableOwners.set(table, pack.name);
2204
+ }
2205
+ registeredPacks.push({
2206
+ name: pack.name,
2207
+ version: pack.version,
2208
+ ownsTables: [...pack.ownsTables],
2209
+ readsTables: [...pack.readsTables ?? []]
2210
+ });
2060
2211
  },
2061
2212
  inspect: () => {
2062
2213
  const collections = [...registry.entries()].map(([name, def]) => {
@@ -2096,6 +2247,12 @@ var createSyncEngine = (options = {}) => {
2096
2247
  version: entry.version,
2097
2248
  table: entry.table,
2098
2249
  op: entry.change.op
2250
+ })),
2251
+ packs: registeredPacks.map((pack) => ({
2252
+ name: pack.name,
2253
+ version: pack.version,
2254
+ ownsTables: [...pack.ownsTables],
2255
+ readsTables: [...pack.readsTables]
2099
2256
  }))
2100
2257
  };
2101
2258
  },
@@ -2182,6 +2339,7 @@ var createSyncEngine = (options = {}) => {
2182
2339
  };
2183
2340
  }
2184
2341
  };
2342
+ return engine;
2185
2343
  };
2186
2344
 
2187
2345
  // src/engine/cdc.ts
@@ -2460,5 +2618,5 @@ export {
2460
2618
  createPresenceHub
2461
2619
  };
2462
2620
 
2463
- //# debugId=DDC1D4CC66583DD464756E2164756E21
2621
+ //# debugId=483AEB81CDF95A5764756E2164756E21
2464
2622
  //# sourceMappingURL=index.js.map