@symbo.ls/sdk 2.32.2 → 2.32.4

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 (90) hide show
  1. package/dist/cjs/config/environment.js +43 -8
  2. package/dist/cjs/index.js +12 -4
  3. package/dist/cjs/services/AdminService.js +4 -4
  4. package/dist/cjs/services/AuthService.js +36 -149
  5. package/dist/cjs/services/BaseService.js +5 -18
  6. package/dist/cjs/services/BranchService.js +10 -10
  7. package/dist/cjs/services/CollabService.js +94 -61
  8. package/dist/cjs/services/CoreService.js +19 -19
  9. package/dist/cjs/services/DnsService.js +4 -4
  10. package/dist/cjs/services/FileService.js +2 -2
  11. package/dist/cjs/services/PaymentService.js +2 -2
  12. package/dist/cjs/services/PlanService.js +12 -12
  13. package/dist/cjs/services/ProjectService.js +45 -35
  14. package/dist/cjs/services/PullRequestService.js +7 -7
  15. package/dist/cjs/services/ScreenshotService.js +304 -0
  16. package/dist/cjs/services/SubscriptionService.js +14 -14
  17. package/dist/cjs/services/index.js +4 -0
  18. package/dist/cjs/utils/TokenManager.js +16 -5
  19. package/dist/cjs/utils/changePreprocessor.js +134 -0
  20. package/dist/cjs/utils/jsonDiff.js +46 -4
  21. package/dist/cjs/utils/ordering.js +274 -0
  22. package/dist/cjs/utils/services.js +14 -1
  23. package/dist/esm/config/environment.js +43 -8
  24. package/dist/esm/index.js +1099 -417
  25. package/dist/esm/services/AdminService.js +68 -35
  26. package/dist/esm/services/AuthService.js +100 -168
  27. package/dist/esm/services/BaseService.js +64 -31
  28. package/dist/esm/services/BranchService.js +74 -41
  29. package/dist/esm/services/CollabService.js +570 -97
  30. package/dist/esm/services/CoreService.js +83 -50
  31. package/dist/esm/services/DnsService.js +68 -35
  32. package/dist/esm/services/FileService.js +66 -33
  33. package/dist/esm/services/PaymentService.js +66 -33
  34. package/dist/esm/services/PlanService.js +76 -43
  35. package/dist/esm/services/ProjectService.js +547 -66
  36. package/dist/esm/services/PullRequestService.js +71 -38
  37. package/dist/esm/services/ScreenshotService.js +992 -0
  38. package/dist/esm/services/SubscriptionService.js +78 -45
  39. package/dist/esm/services/index.js +1076 -412
  40. package/dist/esm/utils/CollabClient.js +89 -12
  41. package/dist/esm/utils/TokenManager.js +16 -5
  42. package/dist/esm/utils/changePreprocessor.js +442 -0
  43. package/dist/esm/utils/jsonDiff.js +46 -4
  44. package/dist/esm/utils/ordering.js +256 -0
  45. package/dist/esm/utils/services.js +14 -1
  46. package/dist/node/config/environment.js +43 -8
  47. package/dist/node/index.js +14 -5
  48. package/dist/node/services/AdminService.js +4 -4
  49. package/dist/node/services/AuthService.js +36 -139
  50. package/dist/node/services/BaseService.js +5 -18
  51. package/dist/node/services/BranchService.js +10 -10
  52. package/dist/node/services/CollabService.js +95 -62
  53. package/dist/node/services/CoreService.js +19 -19
  54. package/dist/node/services/DnsService.js +4 -4
  55. package/dist/node/services/FileService.js +2 -2
  56. package/dist/node/services/PaymentService.js +2 -2
  57. package/dist/node/services/PlanService.js +12 -12
  58. package/dist/node/services/ProjectService.js +45 -35
  59. package/dist/node/services/PullRequestService.js +7 -7
  60. package/dist/node/services/ScreenshotService.js +285 -0
  61. package/dist/node/services/SubscriptionService.js +14 -14
  62. package/dist/node/services/index.js +4 -0
  63. package/dist/node/utils/TokenManager.js +16 -5
  64. package/dist/node/utils/changePreprocessor.js +115 -0
  65. package/dist/node/utils/jsonDiff.js +46 -4
  66. package/dist/node/utils/ordering.js +255 -0
  67. package/dist/node/utils/services.js +14 -1
  68. package/package.json +7 -6
  69. package/src/config/environment.js +48 -9
  70. package/src/index.js +38 -22
  71. package/src/services/AdminService.js +4 -4
  72. package/src/services/AuthService.js +42 -175
  73. package/src/services/BaseService.js +7 -24
  74. package/src/services/BranchService.js +10 -10
  75. package/src/services/CollabService.js +115 -74
  76. package/src/services/CoreService.js +19 -19
  77. package/src/services/DnsService.js +4 -4
  78. package/src/services/FileService.js +2 -2
  79. package/src/services/PaymentService.js +2 -2
  80. package/src/services/PlanService.js +12 -12
  81. package/src/services/ProjectService.js +50 -35
  82. package/src/services/PullRequestService.js +7 -7
  83. package/src/services/ScreenshotService.js +258 -0
  84. package/src/services/SubscriptionService.js +14 -14
  85. package/src/services/index.js +6 -1
  86. package/src/utils/TokenManager.js +19 -5
  87. package/src/utils/changePreprocessor.js +139 -0
  88. package/src/utils/jsonDiff.js +40 -5
  89. package/src/utils/ordering.js +244 -0
  90. package/src/utils/services.js +15 -1
@@ -39,7 +39,7 @@ export class SubscriptionService extends BaseService {
39
39
  }
40
40
  throw new Error(response.message)
41
41
  } catch (error) {
42
- throw new Error(`Failed to create subscription: ${error.message}`)
42
+ throw new Error(`Failed to create subscription: ${error.message}`, { cause: error })
43
43
  }
44
44
  }
45
45
 
@@ -62,7 +62,7 @@ export class SubscriptionService extends BaseService {
62
62
  }
63
63
  throw new Error(response.message)
64
64
  } catch (error) {
65
- throw new Error(`Failed to get project subscription status: ${error.message}`)
65
+ throw new Error(`Failed to get project subscription status: ${error.message}`, { cause: error })
66
66
  }
67
67
  }
68
68
 
@@ -85,7 +85,7 @@ export class SubscriptionService extends BaseService {
85
85
  }
86
86
  throw new Error(response.message)
87
87
  } catch (error) {
88
- throw new Error(`Failed to get subscription usage: ${error.message}`)
88
+ throw new Error(`Failed to get subscription usage: ${error.message}`, { cause: error })
89
89
  }
90
90
  }
91
91
 
@@ -108,7 +108,7 @@ export class SubscriptionService extends BaseService {
108
108
  }
109
109
  throw new Error(response.message)
110
110
  } catch (error) {
111
- throw new Error(`Failed to cancel subscription: ${error.message}`)
111
+ throw new Error(`Failed to cancel subscription: ${error.message}`, { cause: error })
112
112
  }
113
113
  }
114
114
 
@@ -142,7 +142,7 @@ export class SubscriptionService extends BaseService {
142
142
  }
143
143
  throw new Error(response.message)
144
144
  } catch (error) {
145
- throw new Error(`Failed to list invoices: ${error.message}`)
145
+ throw new Error(`Failed to list invoices: ${error.message}`, { cause: error })
146
146
  }
147
147
  }
148
148
 
@@ -171,7 +171,7 @@ export class SubscriptionService extends BaseService {
171
171
  }
172
172
  throw new Error(response.message)
173
173
  } catch (error) {
174
- throw new Error(`Failed to get portal URL: ${error.message}`)
174
+ throw new Error(`Failed to get portal URL: ${error.message}`, { cause: error })
175
175
  }
176
176
  }
177
177
 
@@ -216,7 +216,7 @@ export class SubscriptionService extends BaseService {
216
216
  const status = await this.getProjectStatus(projectId)
217
217
  return status.hasSubscription === true
218
218
  } catch (error) {
219
- throw new Error(`Failed to check subscription status: ${error.message}`)
219
+ throw new Error(`Failed to check subscription status: ${error.message}`, { cause: error })
220
220
  }
221
221
  }
222
222
 
@@ -231,7 +231,7 @@ export class SubscriptionService extends BaseService {
231
231
  }
232
232
  return status.subscription
233
233
  } catch (error) {
234
- throw new Error(`Failed to get project subscription: ${error.message}`)
234
+ throw new Error(`Failed to get project subscription: ${error.message}`, { cause: error })
235
235
  }
236
236
  }
237
237
 
@@ -246,7 +246,7 @@ export class SubscriptionService extends BaseService {
246
246
  }
247
247
  return status.usage
248
248
  } catch (error) {
249
- throw new Error(`Failed to get project usage: ${error.message}`)
249
+ throw new Error(`Failed to get project usage: ${error.message}`, { cause: error })
250
250
  }
251
251
  }
252
252
 
@@ -261,7 +261,7 @@ export class SubscriptionService extends BaseService {
261
261
  pagination: result.pagination || {}
262
262
  }
263
263
  } catch (error) {
264
- throw new Error(`Failed to get invoices with pagination: ${error.message}`)
264
+ throw new Error(`Failed to get invoices with pagination: ${error.message}`, { cause: error })
265
265
  }
266
266
  }
267
267
 
@@ -275,7 +275,7 @@ export class SubscriptionService extends BaseService {
275
275
  // You might need to adjust based on your backend response
276
276
  return usage && usage.subscription && usage.subscription.status === 'active'
277
277
  } catch (error) {
278
- throw new Error(`Failed to check subscription status: ${error.message}`)
278
+ throw new Error(`Failed to check subscription status: ${error.message}`, { cause: error })
279
279
  }
280
280
  }
281
281
 
@@ -287,7 +287,7 @@ export class SubscriptionService extends BaseService {
287
287
  const usage = await this.getUsage(subscriptionId)
288
288
  return usage.limits || {}
289
289
  } catch (error) {
290
- throw new Error(`Failed to get subscription limits: ${error.message}`)
290
+ throw new Error(`Failed to get subscription limits: ${error.message}`, { cause: error })
291
291
  }
292
292
  }
293
293
 
@@ -327,7 +327,7 @@ export class SubscriptionService extends BaseService {
327
327
  }
328
328
  throw new Error(response.message)
329
329
  } catch (error) {
330
- throw new Error(`Failed to change subscription: ${error.message}`)
330
+ throw new Error(`Failed to change subscription: ${error.message}`, { cause: error })
331
331
  }
332
332
  }
333
333
 
@@ -359,7 +359,7 @@ export class SubscriptionService extends BaseService {
359
359
  }
360
360
  throw new Error(response.message)
361
361
  } catch (error) {
362
- throw new Error(`Failed to downgrade subscription: ${error.message}`)
362
+ throw new Error(`Failed to downgrade subscription: ${error.message}`, { cause: error })
363
363
  }
364
364
  }
365
365
 
@@ -11,6 +11,7 @@ import { DnsService } from './DnsService.js'
11
11
  import { BranchService } from './BranchService.js'
12
12
  import { PullRequestService } from './PullRequestService.js'
13
13
  import { AdminService } from './AdminService.js'
14
+ import { ScreenshotService } from './ScreenshotService.js'
14
15
 
15
16
  const createService = (ServiceClass, config) => new ServiceClass(config)
16
17
 
@@ -49,6 +50,9 @@ export const createPullRequestService = config =>
49
50
  export const createAdminService = config =>
50
51
  createService(AdminService, config)
51
52
 
53
+ export const createScreenshotService = config =>
54
+ createService(ScreenshotService, config)
55
+
52
56
  export {
53
57
  AuthService,
54
58
  CoreService,
@@ -61,5 +65,6 @@ export {
61
65
  DnsService,
62
66
  BranchService,
63
67
  PullRequestService,
64
- AdminService
68
+ AdminService,
69
+ ScreenshotService
65
70
  }
@@ -6,7 +6,7 @@ export class TokenManager {
6
6
  constructor (options = {}) {
7
7
  this.config = {
8
8
  storagePrefix: 'symbols_',
9
- storageType: (typeof window === 'undefined' || process.env.NODE_ENV === 'test') ? 'memory' : 'localStorage', // 'localStorage' | 'sessionStorage' | 'memory'
9
+ storageType: (typeof window === 'undefined' || process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'testing') ? 'memory' : 'localStorage', // 'localStorage' | 'sessionStorage' | 'memory'
10
10
  refreshBuffer: 60 * 1000, // Refresh 1 minute before expiry
11
11
  maxRetries: 3,
12
12
  apiUrl: options.apiUrl || '/api',
@@ -52,16 +52,30 @@ export class TokenManager {
52
52
  return this._memoryStorage
53
53
  }
54
54
 
55
- const hasLocalStorage = typeof window.localStorage !== 'undefined'
56
- const hasSessionStorage = typeof window.sessionStorage !== 'undefined'
55
+ // Guard against environments where accessing storage throws (e.g., opaque origins)
56
+ const safeGetStorage = (provider) => {
57
+ try {
58
+ const storage = provider()
59
+ // Try a simple set/remove cycle to ensure it is usable
60
+ const testKey = `${this.config.storagePrefix}__tm_test__`
61
+ storage.setItem(testKey, '1')
62
+ storage.removeItem(testKey)
63
+ return storage
64
+ } catch {
65
+ return null
66
+ }
67
+ }
68
+
69
+ const localStorageInstance = safeGetStorage(() => window.localStorage)
70
+ const sessionStorageInstance = safeGetStorage(() => window.sessionStorage)
57
71
 
58
72
  switch (this.config.storageType) {
59
73
  case 'sessionStorage':
60
- return hasSessionStorage ? window.sessionStorage : this._memoryStorage
74
+ return sessionStorageInstance || this._memoryStorage
61
75
  case 'memory':
62
76
  return this._memoryStorage
63
77
  default:
64
- return hasLocalStorage ? window.localStorage : this._memoryStorage
78
+ return localStorageInstance || this._memoryStorage
65
79
  }
66
80
  }
67
81
 
@@ -0,0 +1,139 @@
1
+
2
+ import { diffJson } from './jsonDiff.js'
3
+ import { computeOrdersForTuples } from './ordering.js'
4
+
5
+ function isPlainObject (val) {
6
+ return val && typeof val === 'object' && !Array.isArray(val)
7
+ }
8
+
9
+ function getByPathSafe (root, path) {
10
+ if (!root || typeof root.getByPath !== 'function') { return null }
11
+ try { return root.getByPath(path) } catch { return null }
12
+ }
13
+
14
+ /**
15
+ * Preprocess broad project changes into granular changes and ordering metadata.
16
+ * - Expands top-level object updates (e.g. ['update', ['components'], {...}])
17
+ * into fine-grained ['update'|'delete', [...], value] tuples using a diff
18
+ * against the current state when available
19
+ * - Preserves schema paths as-is
20
+ * - Filters out explicit deletes targeting __order keys
21
+ * - Appends any extra tuples from options.append
22
+ * - Computes stable orders for impacted parent containers
23
+ */
24
+ export function preprocessChanges (root, tuples = [], options = {}) {
25
+ const expandTuple = (t) => {
26
+ const [action, path, value] = t || []
27
+ const isSchemaPath = Array.isArray(path) && path[0] === 'schema'
28
+ if (action === 'delete') { return [t] }
29
+
30
+ const canConsiderExpansion = (
31
+ action === 'update' &&
32
+ Array.isArray(path) &&
33
+ (
34
+ path.length === 1 ||
35
+ path.length === 2 ||
36
+ (isSchemaPath && path.length === 3)
37
+ ) &&
38
+ isPlainObject(value)
39
+ )
40
+ if (!canConsiderExpansion) { return [t] }
41
+
42
+ const prev = getByPathSafe(root, path) || {}
43
+ const next = value || {}
44
+ if (!isPlainObject(prev) || !isPlainObject(next)) { return [t] }
45
+
46
+ const ops = diffJson(prev, next, [])
47
+ // If diff yields no nested ops, preserve the original tuple as a fallback
48
+ // (e.g. when value equality or missing previous state prevents expansion).
49
+ if (!ops.length) { return [t] }
50
+
51
+ const out = []
52
+ for (let i = 0; i < ops.length; i++) {
53
+ const op = ops[i]
54
+ const fullPath = [...path, ...op.path]
55
+ const last = fullPath[fullPath.length - 1]
56
+ if (op.action === 'set') {
57
+ out.push(['update', fullPath, op.value])
58
+ } else if (op.action === 'del') {
59
+ if (last !== '__order') { out.push(['delete', fullPath]) }
60
+ }
61
+ }
62
+ // Prefer granular leaf operations only to minimize payload duplication.
63
+ return out
64
+ }
65
+
66
+ const minimizeTuples = (input) => {
67
+ const out = []
68
+ const seen = new Set()
69
+ for (let i = 0; i < input.length; i++) {
70
+ const expanded = expandTuple(input[i])
71
+ for (let k = 0; k < expanded.length; k++) {
72
+ const tuple = expanded[k]
73
+ const isDelete = Array.isArray(tuple) && tuple[0] === 'delete'
74
+ const isOrderKey = (
75
+ isDelete &&
76
+ Array.isArray(tuple[1]) &&
77
+ tuple[1][tuple[1].length - 1] === '__order'
78
+ )
79
+ if (!isOrderKey) {
80
+ const key = JSON.stringify(tuple)
81
+ if (!seen.has(key)) { seen.add(key); out.push(tuple) }
82
+ }
83
+ }
84
+ }
85
+ return out
86
+ }
87
+
88
+ const granularChanges = (() => {
89
+ try {
90
+ const res = minimizeTuples(tuples)
91
+ if (options.append && options.append.length) { res.push(...options.append) }
92
+ return res
93
+ } catch {
94
+ // Fallback to original tuples if anything goes wrong
95
+ return Array.isArray(tuples) ? tuples.slice() : []
96
+ }
97
+ })()
98
+
99
+ // Base orders from granular changes/state
100
+ const baseOrders = computeOrdersForTuples(root, granularChanges)
101
+
102
+ // Prefer explicit order for containers updated via ['update', [type], value] or ['update', [type, key], value]
103
+ const preferOrdersMap = new Map()
104
+ for (let i = 0; i < tuples.length; i++) {
105
+ const t = tuples[i]
106
+ if (!Array.isArray(t) || t.length < 3) {
107
+ // eslint-disable-next-line no-continue
108
+ continue
109
+ }
110
+ const [action, path, value] = t
111
+ if (
112
+ action !== 'update' ||
113
+ !Array.isArray(path) ||
114
+ (path.length !== 1 && path.length !== 2) ||
115
+ !isPlainObject(value)
116
+ ) {
117
+ // eslint-disable-next-line no-continue
118
+ continue
119
+ }
120
+ const keys = Object.keys(value).filter(k => k !== '__order')
121
+ const key = JSON.stringify(path)
122
+ preferOrdersMap.set(key, { path, keys })
123
+ }
124
+
125
+ const mergedOrders = []
126
+ const seen = new Set()
127
+ // Add preferred top-level orders first
128
+ preferOrdersMap.forEach((v, k) => { seen.add(k); mergedOrders.push(v) })
129
+ // Add remaining base orders
130
+ for (let i = 0; i < baseOrders.length; i++) {
131
+ const v = baseOrders[i]
132
+ const k = JSON.stringify(v.path)
133
+ if (!seen.has(k)) { seen.add(k); mergedOrders.push(v) }
134
+ }
135
+
136
+ return { granularChanges, orders: mergedOrders }
137
+ }
138
+
139
+
@@ -7,12 +7,47 @@ function isPlainObject (o) {
7
7
  }
8
8
 
9
9
  function deepEqual (a, b) {
10
- try {
11
- return JSON.stringify(a) === JSON.stringify(b)
12
- } catch (err) {
13
- console.warn('deepEqual error', err)
14
- return false
10
+ // Fast path for strict equality (handles primitives and same refs)
11
+ if (Object.is(a, b)) { return true }
12
+
13
+ // Functions: compare source text to detect semantic change
14
+ if (typeof a === 'function' && typeof b === 'function') {
15
+ try { return a.toString() === b.toString() } catch { return false }
15
16
  }
17
+
18
+ // One is function and the other is not
19
+ if (typeof a === 'function' || typeof b === 'function') { return false }
20
+
21
+ // Dates
22
+ if (a instanceof Date && b instanceof Date) { return a.getTime() === b.getTime() }
23
+
24
+ // RegExp
25
+ if (a instanceof RegExp && b instanceof RegExp) { return String(a) === String(b) }
26
+
27
+ // Arrays
28
+ if (Array.isArray(a) && Array.isArray(b)) {
29
+ if (a.length !== b.length) { return false }
30
+ for (let i = 0; i < a.length; i++) {
31
+ if (!deepEqual(a[i], b[i])) { return false }
32
+ }
33
+ return true
34
+ }
35
+
36
+ // Objects (including plain objects when we get here)
37
+ if (a && b && typeof a === 'object' && typeof b === 'object') {
38
+ const aKeys = Object.keys(a)
39
+ const bKeys = Object.keys(b)
40
+ if (aKeys.length !== bKeys.length) { return false }
41
+ for (let i = 0; i < aKeys.length; i++) {
42
+ const key = aKeys[i]
43
+ if (!Object.hasOwn(b, key)) { return false }
44
+ if (!deepEqual(a[key], b[key])) { return false }
45
+ }
46
+ return true
47
+ }
48
+
49
+ // Fallback for different types
50
+ return false
16
51
  }
17
52
 
18
53
  import * as Y from 'yjs'
@@ -0,0 +1,244 @@
1
+ /* eslint-disable no-continue */
2
+ // Utilities to compute stable key ordering for parent objects impacted by changes
3
+
4
+ function isObjectLike (val) {
5
+ return val && typeof val === 'object' && !Array.isArray(val)
6
+ }
7
+
8
+ function normalizePath (path) {
9
+ if (Array.isArray(path)) {return path}
10
+ if (typeof path === 'string') {return [path]}
11
+ return []
12
+ }
13
+
14
+ export function getParentPathsFromTuples (tuples = []) {
15
+ const seen = new Set()
16
+ const parents = []
17
+ const META_KEYS = new Set([
18
+ 'style', 'class', 'text', 'html', 'content', 'data', 'attr', 'state', 'scope',
19
+ 'define', 'on', 'extend', 'extends', 'childExtend', 'childExtends',
20
+ 'children', 'component', 'context', 'tag', 'key', '__order', 'if'
21
+ ])
22
+
23
+ for (let i = 0; i < tuples.length; i++) {
24
+ const tuple = tuples[i]
25
+ if (!Array.isArray(tuple) || tuple.length < 2) {continue}
26
+ const path = normalizePath(tuple[1])
27
+ if (!path.length) {continue}
28
+ // Ignore schema containers entirely for parent order targets
29
+ if (path[0] === 'schema') {continue}
30
+ const immediateParent = path.slice(0, -1)
31
+ if (immediateParent.length) {
32
+ const key = JSON.stringify(immediateParent)
33
+ if (!seen.has(key)) {
34
+ seen.add(key)
35
+ parents.push(immediateParent)
36
+ }
37
+ }
38
+
39
+ // If the tuple points to a meta key (e.g. props/text), also include the container parent
40
+ const last = path[path.length - 1]
41
+ if (META_KEYS.has(last) && path.length >= 2) {
42
+ const containerParent = path.slice(0, -2)
43
+ if (containerParent.length) {
44
+ const key2 = JSON.stringify(containerParent)
45
+ if (!seen.has(key2)) {
46
+ seen.add(key2)
47
+ parents.push(containerParent)
48
+ }
49
+ }
50
+ }
51
+ // Additionally include container parents for any meta segment in the path
52
+ for (let j = 0; j < path.length; j++) {
53
+ const seg = path[j]
54
+ if (!META_KEYS.has(seg)) { continue }
55
+ const containerParent2 = path.slice(0, j)
56
+ if (!containerParent2.length) { continue }
57
+ const key3 = JSON.stringify(containerParent2)
58
+ if (!seen.has(key3)) {
59
+ seen.add(key3)
60
+ parents.push(containerParent2)
61
+ }
62
+ }
63
+ }
64
+
65
+ return parents
66
+ }
67
+
68
+ /**
69
+ * Compute ordered key arrays for each parent path using the provided root state's getByPath.
70
+ *
71
+ * @param {Object} root - Root state with a getByPath(pathArray) method
72
+ * @param {Array<Array<string>>} parentPaths - Array of parent paths to inspect
73
+ * @returns {Array<{ path: string[], keys: string[] }>} orders
74
+ */
75
+ export function computeOrdersFromState (root, parentPaths = []) {
76
+ if (!root || typeof root.getByPath !== 'function') {return []}
77
+
78
+ const orders = []
79
+ const EXCLUDE_KEYS = new Set(['__order'])
80
+
81
+ for (let i = 0; i < parentPaths.length; i++) {
82
+ const parentPath = parentPaths[i]
83
+ const obj = (() => {
84
+ try { return root.getByPath(parentPath) } catch { return null }
85
+ })()
86
+
87
+ if (!isObjectLike(obj)) {continue}
88
+
89
+ const keys = Object.keys(obj).filter(k => !EXCLUDE_KEYS.has(k))
90
+ orders.push({ path: parentPath, keys })
91
+ }
92
+
93
+ return orders
94
+ }
95
+
96
+ /**
97
+ * Convenience helper to derive orders directly from tuples and a root state.
98
+ */
99
+ // --- Schema `code` parsing helpers ---
100
+ function normaliseSchemaCode (code) {
101
+ if (typeof code !== 'string' || !code.length) { return '' }
102
+ // Replace custom placeholders back to actual characters
103
+ return code
104
+ .replaceAll('/////n', '\n')
105
+ .replaceAll('/////tilde', '`')
106
+ }
107
+
108
+ function parseExportedObject (code) {
109
+ const src = normaliseSchemaCode(code)
110
+ if (!src) { return null }
111
+ const body = src.replace(/^\s*export\s+default\s*/u, 'return ')
112
+ try {
113
+ // eslint-disable-next-line no-new-func
114
+ return new Function(body)()
115
+ } catch {
116
+ return null
117
+ }
118
+ }
119
+
120
+ function extractTopLevelKeysFromCode (code) {
121
+ const obj = parseExportedObject(code)
122
+ if (!obj || typeof obj !== 'object') { return [] }
123
+ return Object.keys(obj)
124
+ }
125
+
126
+ export function computeOrdersForTuples (root, tuples = []) {
127
+ // Pre-scan tuples to collect child keys that will be added/updated for each
128
+ // data container (e.g. ['pages', '/']). This lets us include keys created in
129
+ // the same batch even if they are not yet present in the state object when
130
+ // we compute orders.
131
+ const pendingChildrenByContainer = new Map()
132
+ for (let i = 0; i < tuples.length; i++) {
133
+ const t = tuples[i]
134
+ if (!Array.isArray(t)) { continue }
135
+ const [action, path] = t
136
+ const p = normalizePath(path)
137
+ if (!Array.isArray(p) || p.length < 3) { continue }
138
+ // Ignore schema edits here – we want actual data container child keys
139
+ if (p[0] === 'schema') { continue }
140
+ const [typeName, containerKey, childKey] = p
141
+ const containerPath = [typeName, containerKey]
142
+ const key = JSON.stringify(containerPath)
143
+ if (!pendingChildrenByContainer.has(key)) {
144
+ pendingChildrenByContainer.set(key, new Set())
145
+ }
146
+ // We only track updates/sets; deletes need not appear in the desired order
147
+ if (action === 'update' || action === 'set') {
148
+ pendingChildrenByContainer.get(key).add(childKey)
149
+ }
150
+ }
151
+
152
+ // 1) Prefer code-derived order for corresponding data container when schema 'code' present
153
+ const preferredOrderMap = new Map()
154
+ for (let i = 0; i < tuples.length; i++) {
155
+ const t = tuples[i]
156
+ if (!Array.isArray(t)) {continue}
157
+ const [action, path, value] = t
158
+ const p = normalizePath(path)
159
+ if (action !== 'update' || !Array.isArray(p) || p.length < 3) {continue}
160
+ if (p[0] !== 'schema') {continue}
161
+ const [, type, key] = p
162
+ const containerPath = [type, key]
163
+ const uses = value && Array.isArray(value.uses) ? value.uses : null
164
+ const code = value && value.code
165
+
166
+ // Resolve present keys from state
167
+ const obj = (() => {
168
+ try { return root && typeof root.getByPath === 'function' ? root.getByPath(containerPath) : null } catch { return null }
169
+ })()
170
+ if (!obj) {continue}
171
+ const present = new Set(Object.keys(obj))
172
+ const EXCLUDE_KEYS = new Set(['__order'])
173
+
174
+ // Try to parse key order from schema.code
175
+ const codeKeys = extractTopLevelKeysFromCode(code)
176
+ let resolved = []
177
+ // Keys eligible for ordering are those already present OR being added in
178
+ // this same batch of tuples under the same container.
179
+ const pendingKey = JSON.stringify(containerPath)
180
+ const pendingChildren = pendingChildrenByContainer.get(pendingKey) || new Set()
181
+ const eligible = new Set([...present, ...pendingChildren])
182
+
183
+ if (Array.isArray(codeKeys) && codeKeys.length) {
184
+ resolved = codeKeys.filter(k => eligible.has(k) && !EXCLUDE_KEYS.has(k))
185
+ }
186
+ if (Array.isArray(uses) && uses.length) {
187
+ for (let u = 0; u < uses.length; u++) {
188
+ const keyName = uses[u]
189
+ if (eligible.has(keyName) && !EXCLUDE_KEYS.has(keyName) && !resolved.includes(keyName)) {
190
+ resolved.push(keyName)
191
+ }
192
+ }
193
+ }
194
+ // Ensure any pending children not referenced by code/uses still appear
195
+ // after code/uses-derived order, preserving stability.
196
+ if (pendingChildren.size) {
197
+ for (const child of pendingChildren) {
198
+ if (!EXCLUDE_KEYS.has(child) && !resolved.includes(child)) {
199
+ resolved.push(child)
200
+ }
201
+ }
202
+ }
203
+
204
+ if (resolved.length) {
205
+ preferredOrderMap.set(JSON.stringify(containerPath), { path: containerPath, keys: resolved })
206
+ }
207
+ }
208
+
209
+ // 2) Include immediate parent paths from tuples (excluding schema paths)
210
+ const parents = getParentPathsFromTuples(tuples)
211
+
212
+ // 3) Build final orders: prefer schema-derived order when available, otherwise infer from state
213
+ const orders = []
214
+ const seen = new Set()
215
+
216
+ // Add preferred orders first
217
+ preferredOrderMap.forEach(v => {
218
+ const k = JSON.stringify(v.path)
219
+ if (!seen.has(k)) { seen.add(k); orders.push(v) }
220
+ })
221
+
222
+ // Add remaining parents with state-derived order
223
+ const fallbackOrders = computeOrdersFromState(root, parents)
224
+ for (let i = 0; i < fallbackOrders.length; i++) {
225
+ const v = fallbackOrders[i]
226
+ const k = JSON.stringify(v.path)
227
+ if (seen.has(k)) { continue }
228
+ // Merge in any pending children (for containers without schema edits)
229
+ const pending = pendingChildrenByContainer.get(k)
230
+ if (pending && pending.size) {
231
+ const existing = new Set(v.keys)
232
+ for (const child of pending) {
233
+ if (existing.has(child)) { continue }
234
+ v.keys.push(child)
235
+ }
236
+ }
237
+ seen.add(k)
238
+ orders.push(v)
239
+ }
240
+
241
+ return orders
242
+ }
243
+
244
+
@@ -243,5 +243,19 @@ export const SERVICE_METHODS = {
243
243
  demoteFromAdmin: 'admin',
244
244
 
245
245
  // Utility methods
246
- getHealthStatus: 'core'
246
+ getHealthStatus: 'core',
247
+
248
+ // Screenshot methods
249
+ createScreenshotProject: 'screenshot',
250
+ getProjectScreenshots: 'screenshot',
251
+ reprocessProjectScreenshots: 'screenshot',
252
+ recreateProjectScreenshots: 'screenshot',
253
+ deleteProjectScreenshots: 'screenshot',
254
+ getThumbnailCandidate: 'screenshot',
255
+ updateProjectThumbnail: 'screenshot',
256
+ getPageScreenshot: 'screenshot',
257
+ getComponentScreenshot: 'screenshot',
258
+ getScreenshotByKey: 'screenshot',
259
+ getQueueStatistics: 'screenshot',
260
+ refreshThumbnail: 'screenshot'
247
261
  }