@git-stunts/git-warp 12.0.0 → 12.2.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.
Files changed (31) hide show
  1. package/README.md +6 -9
  2. package/bin/warp-graph.js +6 -2
  3. package/index.d.ts +4 -4
  4. package/package.json +2 -1
  5. package/src/domain/WarpGraph.js +3 -0
  6. package/src/domain/crdt/ORSet.js +33 -4
  7. package/src/domain/errors/SyncError.js +1 -0
  8. package/src/domain/errors/TrustError.js +2 -0
  9. package/src/domain/services/CheckpointService.js +2 -7
  10. package/src/domain/services/Frontier.js +18 -0
  11. package/src/domain/services/GraphTraversal.js +8 -49
  12. package/src/domain/services/HttpSyncServer.js +18 -29
  13. package/src/domain/services/JoinReducer.js +23 -0
  14. package/src/domain/services/ObserverView.js +4 -32
  15. package/src/domain/services/PatchBuilderV2.js +29 -3
  16. package/src/domain/services/QueryBuilder.js +78 -74
  17. package/src/domain/services/SyncController.js +74 -11
  18. package/src/domain/services/SyncPayloadSchema.js +236 -0
  19. package/src/domain/services/SyncProtocol.js +27 -8
  20. package/src/domain/services/SyncTrustGate.js +146 -0
  21. package/src/domain/services/TranslationCost.js +8 -24
  22. package/src/domain/trust/TrustRecordService.js +119 -6
  23. package/src/domain/utils/matchGlob.js +51 -0
  24. package/src/domain/warp/Writer.js +7 -5
  25. package/src/domain/warp/checkpoint.methods.js +66 -9
  26. package/src/domain/warp/materialize.methods.js +3 -0
  27. package/src/domain/warp/materializeAdvanced.methods.js +2 -0
  28. package/src/domain/warp/patch.methods.js +8 -0
  29. package/src/domain/warp/query.methods.js +7 -5
  30. package/src/domain/warp/subscribe.methods.js +11 -19
  31. package/src/infrastructure/adapters/GitGraphAdapter.js +2 -2
@@ -530,6 +530,14 @@ export function join(otherState) {
530
530
  // Update cached state
531
531
  this._cachedState = mergedState;
532
532
 
533
+ // Invalidate derived caches (C1) — join changes underlying state
534
+ this._materializedGraph = null;
535
+ this._logicalIndex = null;
536
+ this._propertyReader = null;
537
+ this._cachedViewHash = null;
538
+ this._cachedIndexTree = null;
539
+ this._stateDirty = true;
540
+
533
541
  return { state: mergedState, receipt };
534
542
  }
535
543
 
@@ -312,14 +312,16 @@ export function query() {
312
312
  * @this {import('../WarpGraph.js').default}
313
313
  * @param {string} name - Observer name
314
314
  * @param {Object} config - Observer configuration
315
- * @param {string} config.match - Glob pattern for visible nodes
315
+ * @param {string|string[]} config.match - Glob pattern(s) for visible nodes
316
316
  * @param {string[]} [config.expose] - Property keys to include
317
317
  * @param {string[]} [config.redact] - Property keys to exclude
318
318
  * @returns {Promise<import('../services/ObserverView.js').default>} A read-only observer view
319
319
  */
320
320
  export async function observer(name, config) {
321
- if (!config || typeof config.match !== 'string') {
322
- throw new Error('observer config.match must be a string');
321
+ /** @param {unknown} m */
322
+ const isValidMatch = (m) => typeof m === 'string' || (Array.isArray(m) && m.length > 0 && m.every(/** @param {unknown} i */ i => typeof i === 'string'));
323
+ if (!config || !isValidMatch(config.match)) {
324
+ throw new Error('observer config.match must be a non-empty string or non-empty array of strings');
323
325
  }
324
326
  await this._ensureFreshState();
325
327
  return new ObserverView({ name, config, graph: this });
@@ -330,11 +332,11 @@ export async function observer(name, config) {
330
332
  *
331
333
  * @this {import('../WarpGraph.js').default}
332
334
  * @param {Object} configA - Observer configuration for A
333
- * @param {string} configA.match - Glob pattern for visible nodes
335
+ * @param {string|string[]} configA.match - Glob pattern(s) for visible nodes
334
336
  * @param {string[]} [configA.expose] - Property keys to include
335
337
  * @param {string[]} [configA.redact] - Property keys to exclude
336
338
  * @param {Object} configB - Observer configuration for B
337
- * @param {string} configB.match - Glob pattern for visible nodes
339
+ * @param {string|string[]} configB.match - Glob pattern(s) for visible nodes
338
340
  * @param {string[]} [configB.expose] - Property keys to include
339
341
  * @param {string[]} [configB.redact] - Property keys to exclude
340
342
  * @returns {Promise<{cost: number, breakdown: {nodeLoss: number, edgeLoss: number, propLoss: number}}>}
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { diffStates, isEmptyDiff } from '../services/StateDiff.js';
9
+ import { matchGlob } from '../utils/matchGlob.js';
9
10
 
10
11
  /**
11
12
  * Subscribes to graph changes.
@@ -101,13 +102,13 @@ export function subscribe({ onChange, onError, replay = false }) {
101
102
  * be at least 1000ms.
102
103
  *
103
104
  * @this {import('../WarpGraph.js').default}
104
- * @param {string} pattern - Glob pattern (e.g., 'user:*', 'order:123', '*')
105
+ * @param {string|string[]} pattern - Glob pattern(s) (e.g., 'user:*', 'order:123', '*')
105
106
  * @param {Object} options - Watch options
106
107
  * @param {(diff: import('../services/StateDiff.js').StateDiffResult) => void} options.onChange - Called with filtered diff when matching changes occur
107
108
  * @param {(error: Error) => void} [options.onError] - Called if onChange throws an error
108
109
  * @param {number} [options.poll] - Poll interval in ms (min 1000); checks frontier and auto-materializes
109
110
  * @returns {{unsubscribe: () => void}} Subscription handle
110
- * @throws {Error} If pattern is not a string
111
+ * @throws {Error} If pattern is not a string or array of strings
111
112
  * @throws {Error} If onChange is not a function
112
113
  * @throws {Error} If poll is provided but less than 1000
113
114
  *
@@ -130,31 +131,22 @@ export function subscribe({ onChange, onError, replay = false }) {
130
131
  * unsubscribe();
131
132
  */
132
133
  export function watch(pattern, { onChange, onError, poll }) {
133
- if (typeof pattern !== 'string') {
134
- throw new Error('pattern must be a string');
134
+ const isValidPattern = (/** @type {string|string[]} */ p) => typeof p === 'string' || (Array.isArray(p) && p.length > 0 && p.every(i => typeof i === 'string'));
135
+ if (!isValidPattern(pattern)) {
136
+ throw new Error('pattern must be a non-empty string or non-empty array of strings');
135
137
  }
136
138
  if (typeof onChange !== 'function') {
137
139
  throw new Error('onChange must be a function');
138
140
  }
139
141
  if (poll !== undefined) {
140
- if (typeof poll !== 'number' || poll < 1000) {
141
- throw new Error('poll must be a number >= 1000');
142
+ if (typeof poll !== 'number' || !Number.isFinite(poll) || poll < 1000) {
143
+ throw new Error('poll must be a finite number >= 1000');
142
144
  }
143
145
  }
144
146
 
145
- // Pattern matching: same logic as QueryBuilder.match()
146
- // Pre-compile pattern matcher once for performance
147
+ // Pattern matching logic
147
148
  /** @type {(nodeId: string) => boolean} */
148
- let matchesPattern;
149
- if (pattern === '*') {
150
- matchesPattern = () => true;
151
- } else if (pattern.includes('*')) {
152
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
153
- const regex = new RegExp(`^${escaped.replace(/\*/g, '.*')}$`);
154
- matchesPattern = (/** @type {string} */ nodeId) => regex.test(nodeId);
155
- } else {
156
- matchesPattern = (/** @type {string} */ nodeId) => nodeId === pattern;
157
- }
149
+ const matchesPattern = (nodeId) => matchGlob(pattern, nodeId);
158
150
 
159
151
  // Filtered onChange that only passes matching changes
160
152
  const filteredOnChange = (/** @type {import('../services/StateDiff.js').StateDiffResult} */ diff) => {
@@ -194,7 +186,7 @@ export function watch(pattern, { onChange, onError, poll }) {
194
186
  /** @type {ReturnType<typeof setInterval>|null} */
195
187
  let pollIntervalId = null;
196
188
  let pollInFlight = false;
197
- if (poll) {
189
+ if (poll !== undefined) {
198
190
  pollIntervalId = setInterval(() => {
199
191
  if (pollInFlight) {
200
192
  return;
@@ -575,8 +575,8 @@ export default class GitGraphAdapter extends GraphPersistencePort {
575
575
  async compareAndSwapRef(ref, newOid, expectedOid) {
576
576
  this._validateRef(ref);
577
577
  this._validateOid(newOid);
578
- // null means "ref must not exist" → use zero OID
579
- const oldArg = expectedOid || '0'.repeat(newOid.length);
578
+ // null means "ref must not exist" → use zero OID (always 40 chars for SHA-1)
579
+ const oldArg = expectedOid || '0'.repeat(40);
580
580
  if (expectedOid) {
581
581
  this._validateOid(expectedOid);
582
582
  }