@aephia/atlas-kit 0.4.0 → 0.6.0-next.1

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 (161) hide show
  1. package/README.md +49 -29
  2. package/dist/cargo/index.d.ts +1 -0
  3. package/dist/cargo/index.d.ts.map +1 -1
  4. package/dist/cargo/index.js +1 -0
  5. package/dist/cargo/index.js.map +1 -1
  6. package/dist/claim-stakes/actions/index.d.ts +393 -0
  7. package/dist/claim-stakes/actions/index.d.ts.map +1 -0
  8. package/dist/claim-stakes/actions/index.js +1747 -0
  9. package/dist/claim-stakes/actions/index.js.map +1 -0
  10. package/dist/claim-stakes/index.d.ts +64 -0
  11. package/dist/claim-stakes/index.d.ts.map +1 -1
  12. package/dist/claim-stakes/index.js +49 -0
  13. package/dist/claim-stakes/index.js.map +1 -1
  14. package/dist/client/context.d.ts.map +1 -1
  15. package/dist/client/context.js +2 -1
  16. package/dist/client/context.js.map +1 -1
  17. package/dist/client/errors.d.ts +12 -3
  18. package/dist/client/errors.d.ts.map +1 -1
  19. package/dist/client/errors.js +11 -0
  20. package/dist/client/errors.js.map +1 -1
  21. package/dist/client/index.d.ts +2 -1
  22. package/dist/client/index.d.ts.map +1 -1
  23. package/dist/client/index.js +1 -0
  24. package/dist/client/index.js.map +1 -1
  25. package/dist/client/lifecycle.d.ts +2 -0
  26. package/dist/client/lifecycle.d.ts.map +1 -1
  27. package/dist/client/lifecycle.js +31 -0
  28. package/dist/client/lifecycle.js.map +1 -1
  29. package/dist/combat/actions/index.d.ts +239 -0
  30. package/dist/combat/actions/index.d.ts.map +1 -0
  31. package/dist/combat/actions/index.js +1146 -0
  32. package/dist/combat/actions/index.js.map +1 -0
  33. package/dist/crafting/actions/index.d.ts +140 -0
  34. package/dist/crafting/actions/index.d.ts.map +1 -0
  35. package/dist/crafting/actions/index.js +728 -0
  36. package/dist/crafting/actions/index.js.map +1 -0
  37. package/dist/crafting/index.d.ts +74 -8
  38. package/dist/crafting/index.d.ts.map +1 -1
  39. package/dist/crafting/index.js +55 -9
  40. package/dist/crafting/index.js.map +1 -1
  41. package/dist/fleets/actions/index.d.ts +5 -4
  42. package/dist/fleets/actions/index.d.ts.map +1 -1
  43. package/dist/fleets/actions/index.js +85 -58
  44. package/dist/fleets/actions/index.js.map +1 -1
  45. package/dist/fleets/index.d.ts +1 -0
  46. package/dist/fleets/index.d.ts.map +1 -1
  47. package/dist/fleets/index.js +1 -0
  48. package/dist/fleets/index.js.map +1 -1
  49. package/dist/index.d.ts +13 -2
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +9 -2
  52. package/dist/index.js.map +1 -1
  53. package/dist/internal/c4/claim-stake-building-definitions.d.ts +50 -0
  54. package/dist/internal/c4/claim-stake-building-definitions.d.ts.map +1 -0
  55. package/dist/internal/c4/claim-stake-building-definitions.js +56 -0
  56. package/dist/internal/c4/claim-stake-building-definitions.js.map +1 -0
  57. package/dist/internal/c4/claim-stake-configuration.d.ts +17 -0
  58. package/dist/internal/c4/claim-stake-configuration.d.ts.map +1 -0
  59. package/dist/internal/c4/claim-stake-configuration.js +47 -0
  60. package/dist/internal/c4/claim-stake-configuration.js.map +1 -0
  61. package/dist/internal/c4/claim-stake-definitions.d.ts +37 -0
  62. package/dist/internal/c4/claim-stake-definitions.d.ts.map +1 -0
  63. package/dist/internal/c4/claim-stake-definitions.js +77 -0
  64. package/dist/internal/c4/claim-stake-definitions.js.map +1 -0
  65. package/dist/internal/c4/fleet.d.ts.map +1 -1
  66. package/dist/internal/c4/fleet.js +2 -102
  67. package/dist/internal/c4/fleet.js.map +1 -1
  68. package/dist/internal/c4/game-definition-layout.d.ts +11 -0
  69. package/dist/internal/c4/game-definition-layout.d.ts.map +1 -0
  70. package/dist/internal/c4/game-definition-layout.js +32 -0
  71. package/dist/internal/c4/game-definition-layout.js.map +1 -0
  72. package/dist/internal/c4/game.d.ts +13 -1
  73. package/dist/internal/c4/game.d.ts.map +1 -1
  74. package/dist/internal/c4/game.js +19 -37
  75. package/dist/internal/c4/game.js.map +1 -1
  76. package/dist/internal/c4/profile-faction.d.ts +6 -0
  77. package/dist/internal/c4/profile-faction.d.ts.map +1 -1
  78. package/dist/internal/c4/profile-faction.js +22 -1
  79. package/dist/internal/c4/profile-faction.js.map +1 -1
  80. package/dist/internal/c4/ship-stats.d.ts +5 -0
  81. package/dist/internal/c4/ship-stats.d.ts.map +1 -0
  82. package/dist/internal/c4/ship-stats.js +106 -0
  83. package/dist/internal/c4/ship-stats.js.map +1 -0
  84. package/dist/internal/c4/world.d.ts +23 -1
  85. package/dist/internal/c4/world.d.ts.map +1 -1
  86. package/dist/internal/c4/world.js +53 -1
  87. package/dist/internal/c4/world.js.map +1 -1
  88. package/dist/internal/crafting-process-state.d.ts +15 -0
  89. package/dist/internal/crafting-process-state.d.ts.map +1 -0
  90. package/dist/internal/crafting-process-state.js +12 -0
  91. package/dist/internal/crafting-process-state.js.map +1 -0
  92. package/dist/internal/planning/authorization-adapter.d.ts +74 -0
  93. package/dist/internal/planning/authorization-adapter.d.ts.map +1 -1
  94. package/dist/internal/planning/authorization-adapter.js +382 -0
  95. package/dist/internal/planning/authorization-adapter.js.map +1 -1
  96. package/dist/internal/read-meta.d.ts +39 -3
  97. package/dist/internal/read-meta.d.ts.map +1 -1
  98. package/dist/internal/read-meta.js +0 -10
  99. package/dist/internal/read-meta.js.map +1 -1
  100. package/dist/loyalty/actions/index.d.ts +40 -0
  101. package/dist/loyalty/actions/index.d.ts.map +1 -0
  102. package/dist/loyalty/actions/index.js +159 -0
  103. package/dist/loyalty/actions/index.js.map +1 -0
  104. package/dist/markets/actions/index.d.ts +100 -0
  105. package/dist/markets/actions/index.d.ts.map +1 -0
  106. package/dist/markets/actions/index.js +417 -0
  107. package/dist/markets/actions/index.js.map +1 -0
  108. package/dist/planning/assembly.d.ts +4 -0
  109. package/dist/planning/assembly.d.ts.map +1 -1
  110. package/dist/planning/assembly.js +13 -0
  111. package/dist/planning/assembly.js.map +1 -1
  112. package/dist/planning/composition.d.ts.map +1 -1
  113. package/dist/planning/composition.js +21 -2
  114. package/dist/planning/composition.js.map +1 -1
  115. package/dist/planning/execution.d.ts.map +1 -1
  116. package/dist/planning/execution.js +7 -4
  117. package/dist/planning/execution.js.map +1 -1
  118. package/dist/planning/index.d.ts +27 -8
  119. package/dist/planning/index.d.ts.map +1 -1
  120. package/dist/planning/index.js +57 -13
  121. package/dist/planning/index.js.map +1 -1
  122. package/dist/planning/plan-sequence-execution.d.ts +26 -4
  123. package/dist/planning/plan-sequence-execution.d.ts.map +1 -1
  124. package/dist/planning/plan-sequence-execution.js +26 -4
  125. package/dist/planning/plan-sequence-execution.js.map +1 -1
  126. package/dist/planning/plan-sequence.d.ts +20 -3
  127. package/dist/planning/plan-sequence.d.ts.map +1 -1
  128. package/dist/planning/plan-sequence.js +20 -3
  129. package/dist/planning/plan-sequence.js.map +1 -1
  130. package/dist/planning/simulation.d.ts.map +1 -1
  131. package/dist/planning/simulation.js +2 -1
  132. package/dist/planning/simulation.js.map +1 -1
  133. package/dist/registry/cargo.d.ts +10 -0
  134. package/dist/registry/cargo.d.ts.map +1 -0
  135. package/dist/registry/cargo.js +15 -0
  136. package/dist/registry/cargo.js.map +1 -0
  137. package/dist/registry/claim-stakes.d.ts +11 -0
  138. package/dist/registry/claim-stakes.d.ts.map +1 -0
  139. package/dist/registry/claim-stakes.js +15 -0
  140. package/dist/registry/claim-stakes.js.map +1 -0
  141. package/dist/registry/definition-section-snapshot.d.ts +42 -0
  142. package/dist/registry/definition-section-snapshot.d.ts.map +1 -0
  143. package/dist/registry/definition-section-snapshot.js +214 -0
  144. package/dist/registry/definition-section-snapshot.js.map +1 -0
  145. package/dist/registry/index.d.ts +114 -7
  146. package/dist/registry/index.d.ts.map +1 -1
  147. package/dist/registry/index.js +372 -101
  148. package/dist/registry/index.js.map +1 -1
  149. package/dist/registry/publication.d.ts +8 -0
  150. package/dist/registry/publication.d.ts.map +1 -0
  151. package/dist/registry/publication.js +10 -0
  152. package/dist/registry/publication.js.map +1 -0
  153. package/dist/registry/ship-stats-snapshot.d.ts +8 -0
  154. package/dist/registry/ship-stats-snapshot.d.ts.map +1 -0
  155. package/dist/registry/ship-stats-snapshot.js +154 -0
  156. package/dist/registry/ship-stats-snapshot.js.map +1 -0
  157. package/dist/world/index.d.ts +22 -1
  158. package/dist/world/index.d.ts.map +1 -1
  159. package/dist/world/index.js +24 -1
  160. package/dist/world/index.js.map +1 -1
  161. package/package.json +21 -1
@@ -1,10 +1,16 @@
1
+ import { encodeDefinitionSectionSnapshot, hydrateDefinitionSectionSnapshot, } from './definition-section-snapshot.js';
2
+ import { captureSectionRevisions, publishIfCurrent } from './publication.js';
3
+ import { attachReadMeta, } from '../internal/read-meta.js';
4
+ import { attachJsonSerialization, toJsonSafeValue } from '../internal/json.js';
5
+ import { hydrateShipStats } from './ship-stats-snapshot.js';
1
6
  import { isAddress } from '@solana/kit';
2
- import { AccountNotFoundError, MissingGameContextError, ProviderError, RegistryOutOfSyncError, } from '../client/errors.js';
3
- import { assertContextActive } from '../client/lifecycle.js';
7
+ import { AccountNotFoundError, MissingGameContextError, InvalidDefinitionSectionError, ProviderError, RegistryOutOfSyncError, } from '../client/errors.js';
8
+ import { assertContextActive, waitForRegistry } from '../client/lifecycle.js';
4
9
  import { deepFreeze, validateImmutableTree, } from '../internal/immutable-tree.js';
5
10
  import { decodeGameRegistrySections, } from '../internal/c4/game.js';
6
11
  const SEQUENCE_MISMATCH_RETRY_DELAY_MS = 1_000;
7
- // Ship summaries expose stable identity fields; every other section is strict.
12
+ // D029/D055 tolerate newer ship definitions for older callers; the resolved
13
+ // definitionSequenceId exposes that skew. Every other section remains strict.
8
14
  const SECTIONS_TOLERATING_OLDER_CALLERS = new Set(['ships']);
9
15
  const registryStates = new WeakMap();
10
16
  function stateFor(context) {
@@ -15,7 +21,7 @@ function stateFor(context) {
15
21
  confirmedSequenceMismatches: {},
16
22
  confirmingMissing: new Set(),
17
23
  confirmingSequenceIds: {},
18
- loading: {},
24
+ revisions: {},
19
25
  refresh: new Set(),
20
26
  sections: {},
21
27
  };
@@ -26,7 +32,33 @@ function stateFor(context) {
26
32
  function freezeDefinitions(definitions) {
27
33
  return Object.freeze(definitions.map((definition) => deepFreeze({ ...definition })));
28
34
  }
35
+ function freezeShips(definitions, sequenceId) {
36
+ return Object.freeze(definitions.map((definition) => {
37
+ const value = deepFreeze({
38
+ ...definition,
39
+ definitionSequenceId: sequenceId,
40
+ });
41
+ return Object.freeze(attachJsonSerialization({ ...value }));
42
+ }));
43
+ }
44
+ function isPersistedDefinitionSection(section) {
45
+ return (section === 'claimStakes' ||
46
+ section === 'buildings' ||
47
+ section === 'craftingHabs' ||
48
+ section === 'habBuildings');
49
+ }
50
+ function freezeCatalog(data) {
51
+ const catalog = Object.freeze(data.definitions.map((definition) => Object.freeze(attachJsonSerialization({ ...deepFreeze(definition) }))));
52
+ return {
53
+ catalog,
54
+ definitions: new Map(catalog.map((definition) => [definition.id, definition])),
55
+ sequenceId: data.sequenceId,
56
+ };
57
+ }
29
58
  function sectionFromGame(game, section) {
59
+ if (isPersistedDefinitionSection(section)) {
60
+ return freezeCatalog(game);
61
+ }
30
62
  if (section === 'xp') {
31
63
  const definitions = freezeDefinitions([
32
64
  game,
@@ -46,11 +78,13 @@ function sectionFromGame(game, section) {
46
78
  }
47
79
  if (section === 'ships') {
48
80
  const ships = game;
49
- const definitions = freezeDefinitions(ships.definitions);
50
- return {
81
+ const definitions = freezeShips(ships.definitions, ships.sequenceId);
82
+ const loaded = {
83
+ configurations: definitions,
51
84
  definitions: new Map(definitions.map((definition) => [definition.id, definition])),
52
85
  sequenceId: ships.sequenceId,
53
86
  };
87
+ return loaded;
54
88
  }
55
89
  const values = game;
56
90
  const definitions = freezeDefinitions(values.definitions);
@@ -84,8 +118,9 @@ function hasUniqueDefinitionIds(definitions) {
84
118
  }
85
119
  function isShipDefinition(value) {
86
120
  return (isRecord(value) &&
87
- hasOwnKeyCount(value, 4) &&
121
+ hasOwnKeyCount(value, 5) &&
88
122
  isNonnegativeInteger(value.id) &&
123
+ value.id <= 65535 &&
89
124
  typeof value.mint === 'string' &&
90
125
  isAddress(value.mint) &&
91
126
  typeof value.name === 'string' &&
@@ -110,10 +145,11 @@ function isCompatibleSnapshot(snapshot, game, section) {
110
145
  }
111
146
  if (!isRecord(snapshot) ||
112
147
  !hasOwnKeyCount(snapshot, 5) ||
113
- snapshot.schemaVersion !== 1 ||
148
+ snapshot.schemaVersion !== (section === 'ships' ? 2 : 1) ||
114
149
  snapshot.game !== game ||
115
150
  snapshot.section !== section ||
116
151
  !isNonnegativeInteger(snapshot.sequenceId) ||
152
+ (section === 'ships' && snapshot.sequenceId > 4294967295) ||
117
153
  !Array.isArray(snapshot.definitions)) {
118
154
  return false;
119
155
  }
@@ -125,18 +161,43 @@ function isCompatibleSnapshot(snapshot, game, section) {
125
161
  hasUniqueDefinitionIds(snapshot.definitions));
126
162
  }
127
163
  function snapshotFor(game, section, loaded) {
164
+ if (section === 'ships') {
165
+ return {
166
+ definitions: [...loaded.definitions.values()].map(({ id, mint, name, sizeClass, stats }) => ({
167
+ id,
168
+ mint,
169
+ name,
170
+ sizeClass,
171
+ stats: toJsonSafeValue(stats),
172
+ })),
173
+ game,
174
+ schemaVersion: 2,
175
+ section,
176
+ sequenceId: loaded.sequenceId,
177
+ };
178
+ }
128
179
  return {
129
- definitions: [...loaded.definitions.values()],
180
+ definitions: [
181
+ ...loaded.definitions.values(),
182
+ ],
130
183
  game,
131
184
  schemaVersion: 1,
132
- section,
185
+ section: 'cargo',
133
186
  sequenceId: loaded.sequenceId,
134
187
  };
135
188
  }
136
189
  function sectionFromSnapshot(snapshot) {
137
190
  if (snapshot.section === 'ships') {
138
- const definitions = freezeDefinitions(snapshot.definitions);
191
+ const rows = [];
192
+ for (const definition of snapshot.definitions) {
193
+ const stats = hydrateShipStats(definition.stats);
194
+ if (stats === undefined)
195
+ return undefined;
196
+ rows.push({ ...definition, stats });
197
+ }
198
+ const definitions = freezeShips(rows, snapshot.sequenceId);
139
199
  return {
200
+ configurations: definitions,
140
201
  definitions: new Map(definitions.map((definition) => [definition.id, definition])),
141
202
  sequenceId: snapshot.sequenceId,
142
203
  };
@@ -148,35 +209,49 @@ function sectionFromSnapshot(snapshot) {
148
209
  };
149
210
  }
150
211
  async function loadStoredSection(context, game, section) {
151
- // Snapshot schema v1 is JSON-native and intentionally excludes crafting
152
- // sections whose exact public definitions contain bigint values.
153
- if (section !== 'ships' && section !== 'cargo') {
212
+ assertContextActive(context);
213
+ if (section === 'xp' || section === 'research')
154
214
  return undefined;
155
- }
156
215
  const store = context.registrySnapshotStore;
157
216
  if (store === undefined)
158
217
  return undefined;
218
+ let snapshot;
159
219
  try {
160
- const snapshot = await store.load(game, section);
161
- if (!isCompatibleSnapshot(snapshot, game, section)) {
162
- return undefined;
163
- }
164
- return sectionFromSnapshot(snapshot);
220
+ snapshot = await store.load(game, section);
165
221
  }
166
222
  catch {
167
223
  throw new ProviderError(`The application registry snapshot store could not load the ${section} definitions. Repair or disable the snapshot store, then retry.`, { accountType: 'game', address: game });
168
224
  }
225
+ if (isPersistedDefinitionSection(section)) {
226
+ const hydrated = hydrateDefinitionSectionSnapshot(snapshot, game, section);
227
+ return hydrated === undefined
228
+ ? undefined
229
+ : freezeCatalog(hydrated);
230
+ }
231
+ if (!isCompatibleSnapshot(snapshot, game, section))
232
+ return undefined;
233
+ return sectionFromSnapshot(snapshot);
169
234
  }
170
235
  async function saveSection(context, game, section, loaded) {
171
- // Keep the load/save boundary symmetric until a versioned bigint encoding
172
- // extends DefinitionSectionSnapshot.
173
- if (section !== 'ships' && section !== 'cargo')
236
+ assertContextActive(context);
237
+ if (section === 'xp' || section === 'research')
174
238
  return;
175
239
  const store = context.registrySnapshotStore;
176
240
  if (store === undefined)
177
241
  return;
178
242
  try {
179
- if (section === 'ships') {
243
+ if (isPersistedDefinitionSection(section)) {
244
+ const definitions = loaded.definitions;
245
+ const snapshot = encodeDefinitionSectionSnapshot(game, section, {
246
+ definitions: [...definitions.values()],
247
+ sequenceId: loaded.sequenceId,
248
+ });
249
+ // Loaded Game projections have already passed the pinned section validator.
250
+ /* v8 ignore next -- the encoder also guards its typed boundary; incompatible inputs are covered in its dedicated tests. */
251
+ if (snapshot !== undefined)
252
+ await store.save(snapshot);
253
+ }
254
+ else if (section === 'ships') {
180
255
  await store.save(snapshotFor(game, section, loaded));
181
256
  }
182
257
  else {
@@ -188,122 +263,209 @@ async function saveSection(context, game, section, loaded) {
188
263
  // read remains authoritative when application-owned storage is unavailable.
189
264
  }
190
265
  }
266
+ function publishSection(state, section, loaded) {
267
+ // Every caller pairs a candidate with the section that translated it.
268
+ state.sections[section] = loaded;
269
+ state.revisions[section] = (state.revisions[section] ?? 0) + 1;
270
+ }
271
+ function invalidateSection(state, section) {
272
+ // Resolver invalidation follows a successful publication, which initializes this revision.
273
+ state.revisions[section] = state.revisions[section] + 1;
274
+ state.refresh.add(section);
275
+ }
191
276
  async function populateFromGame(context, game, sectionsToPopulate, state) {
192
- const account = await context.data.readRaw(game);
277
+ const revisions = captureSectionRevisions(state.revisions, sectionsToPopulate);
278
+ const account = await waitForRegistry(context, context.data.readRaw(game));
279
+ assertContextActive(context);
193
280
  if (account === undefined)
194
281
  throw new AccountNotFoundError('game', game);
195
282
  const decoded = decodeGameRegistrySections(account, sectionsToPopulate);
196
283
  const populated = sectionsToPopulate.map((populatedSection) => {
197
284
  const loaded = (() => {
285
+ if (populatedSection === 'buildings') {
286
+ const buildings = sectionFromGame(decoded.buildings, 'buildings');
287
+ return buildings;
288
+ }
289
+ if (populatedSection === 'claimStakes') {
290
+ const claimStakes = sectionFromGame(decoded.claimStakes, 'claimStakes');
291
+ return claimStakes;
292
+ }
198
293
  if (populatedSection === 'ships') {
199
294
  const ships = sectionFromGame(decoded.ships, 'ships');
200
- state.sections.ships = ships;
201
295
  return ships;
202
296
  }
203
297
  if (populatedSection === 'cargo') {
204
298
  const cargo = sectionFromGame(decoded.cargo, 'cargo');
205
- state.sections.cargo = cargo;
206
299
  return cargo;
207
300
  }
208
301
  if (populatedSection === 'craftingHabs') {
209
302
  const craftingHabs = sectionFromGame(decoded.craftingHabs, 'craftingHabs');
210
- state.sections.craftingHabs = craftingHabs;
211
303
  return craftingHabs;
212
304
  }
213
305
  if (populatedSection === 'habBuildings') {
214
306
  const habBuildings = sectionFromGame(decoded.habBuildings, 'habBuildings');
215
- state.sections.habBuildings = habBuildings;
216
307
  return habBuildings;
217
308
  }
218
309
  if (populatedSection === 'xp') {
219
310
  const xp = sectionFromGame(decoded.xp, 'xp');
220
- state.sections.xp = xp;
221
311
  return xp;
222
312
  }
223
313
  const research = sectionFromGame(decoded.research, 'research');
224
- state.sections.research = research;
225
314
  return research;
226
315
  })();
227
- const confirmingSequenceId = state.confirmingSequenceIds[populatedSection];
228
- if (confirmingSequenceId !== undefined) {
229
- state.confirmedSequenceMismatches[populatedSection] = {
230
- actualSequenceId: loaded.sequenceId,
231
- confirmedAtMonotonicMs: performance.now(),
232
- expectedSequenceId: confirmingSequenceId,
233
- };
234
- delete state.confirmingSequenceIds[populatedSection];
235
- }
236
- else if (state.confirmedSequenceMismatches[populatedSection]?.actualSequenceId !==
237
- loaded.sequenceId) {
238
- delete state.confirmedSequenceMismatches[populatedSection];
239
- }
240
- if (state.confirmingMissing.has(populatedSection)) {
241
- state.confirmedMissingSequenceIds[populatedSection] = loaded.sequenceId;
242
- state.confirmingMissing.delete(populatedSection);
316
+ return {
317
+ loaded: {
318
+ ...loaded,
319
+ observation: Object.freeze({
320
+ slot: account.slot,
321
+ commitment: account.commitment,
322
+ }),
323
+ },
324
+ section: populatedSection,
325
+ };
326
+ });
327
+ assertContextActive(context);
328
+ const saves = [];
329
+ publishIfCurrent(state.revisions, revisions, () => {
330
+ for (const { loaded, section: populatedSection } of populated) {
331
+ publishSection(state, populatedSection, loaded);
332
+ const confirmingSequenceId = state.confirmingSequenceIds[populatedSection];
333
+ if (confirmingSequenceId !== undefined) {
334
+ state.confirmedSequenceMismatches[populatedSection] = {
335
+ actualSequenceId: loaded.sequenceId,
336
+ confirmedAtMonotonicMs: performance.now(),
337
+ expectedSequenceId: confirmingSequenceId,
338
+ };
339
+ delete state.confirmingSequenceIds[populatedSection];
340
+ }
341
+ else if (state.confirmedSequenceMismatches[populatedSection]
342
+ ?.actualSequenceId !== loaded.sequenceId) {
343
+ delete state.confirmedSequenceMismatches[populatedSection];
344
+ }
345
+ if (state.confirmingMissing.has(populatedSection)) {
346
+ state.confirmedMissingSequenceIds[populatedSection] = loaded.sequenceId;
347
+ state.confirmingMissing.delete(populatedSection);
348
+ }
349
+ state.refresh.delete(populatedSection);
243
350
  }
244
- state.refresh.delete(populatedSection);
245
- return { loaded, section: populatedSection };
351
+ for (const { loaded, section } of populated)
352
+ saves.push(saveSection(context, game, section, loaded));
246
353
  });
247
- await Promise.all(populated.map(({ loaded, section }) => saveSection(context, game, section, loaded)));
248
- }
249
- async function populateSection(context, game, section, state) {
250
- if (!state.refresh.has(section)) {
251
- const stored = await loadStoredSection(context, game, section);
252
- if (stored !== undefined) {
253
- if (section === 'ships')
254
- state.sections.ships = stored;
255
- else
256
- state.sections.cargo = stored;
354
+ await waitForRegistry(context, Promise.all(saves));
355
+ }
356
+ async function populateSections(context, game, sections, state, fetchedTargets) {
357
+ const revisions = captureSectionRevisions(state.revisions, sections);
358
+ const stored = await waitForRegistry(context, Promise.all(sections.map(async (section) => state.refresh.has(section)
359
+ ? undefined
360
+ : loadStoredSection(context, game, section))));
361
+ assertContextActive(context);
362
+ for (const [index, section] of sections.entries()) {
363
+ const candidate = stored[index];
364
+ if (candidate !== undefined)
365
+ publishIfCurrent(state.revisions, [revisions[index]], () => publishSection(state, section, candidate));
366
+ }
367
+ const missing = sections.filter((section) => state.sections[section] === undefined || state.refresh.has(section));
368
+ if (missing.length > 0)
369
+ await populateFromGame(context, game, fetchedTargets.filter((section) => state.sections[section] === undefined || state.refresh.has(section)), state);
370
+ }
371
+ async function ensureSections(context, sections, lazyPair = false) {
372
+ const state = stateFor(context);
373
+ for (;;) {
374
+ assertContextActive(context);
375
+ const game = context.game;
376
+ if (game === undefined)
377
+ throw new MissingGameContextError();
378
+ const missing = sections.filter((section) => state.sections[section] === undefined || state.refresh.has(section));
379
+ if (missing.length === 0)
257
380
  return;
381
+ let group = state.gameLoading;
382
+ if (group === undefined) {
383
+ const targets = lazyPair &&
384
+ (missing[0] === 'ships' || missing[0] === 'cargo') &&
385
+ ((state.refresh.has('ships') && state.refresh.has('cargo')) ||
386
+ (state.sections.ships === undefined &&
387
+ state.sections.cargo === undefined))
388
+ ? ['ships', 'cargo']
389
+ : missing;
390
+ const promise = Promise.resolve()
391
+ .then(() => populateSections(context, game, missing, state, targets))
392
+ .finally(() => {
393
+ /* v8 ignore next -- serialization prevents replacement before settlement; retain identity-checked cleanup so a late completion can never clear newer work. */
394
+ if (state.gameLoading?.[0] === promise)
395
+ delete state.gameLoading;
396
+ });
397
+ group = [promise, targets];
398
+ state.gameLoading = group;
258
399
  }
259
- }
260
- let gameLoading = state.gameLoading;
261
- if (gameLoading === undefined) {
262
- const sections = (section === 'ships' || section === 'cargo') &&
263
- ((state.refresh.has('ships') && state.refresh.has('cargo')) ||
264
- (state.sections.ships === undefined &&
265
- state.sections.cargo === undefined))
266
- ? ['ships', 'cargo']
267
- : [section];
268
- const promise = populateFromGame(context, game, sections, state).finally(() => {
269
- delete state.gameLoading;
270
- });
271
- gameLoading = [promise, sections];
272
- state.gameLoading = gameLoading;
273
- }
274
- else if (!gameLoading[1].includes(section)) {
275
400
  try {
276
- await gameLoading[0];
401
+ await waitForRegistry(context, group[0]);
277
402
  }
278
- catch {
279
- // An unrelated population cannot satisfy or fail this section. Retry
280
- // after it settles so Game reads remain serialized.
403
+ catch (error) {
404
+ // Only unrelated work can fail independently of this request.
405
+ if (missing.some((section) => group[1].includes(section)))
406
+ throw error;
281
407
  }
282
- return populateSection(context, game, section, state);
283
- }
284
- await gameLoading[0];
285
- if (state.sections[section] === undefined || state.refresh.has(section)) {
286
- return populateSection(context, game, section, state);
287
408
  }
288
409
  }
289
410
  async function ensureSection(context, section) {
411
+ await ensureSections(context, [section], true);
412
+ return stateFor(context).sections[section];
413
+ }
414
+ /** Warm selected definition catalogs, retaining each section's own observation.
415
+ * Already usable sections are reused; this is not a freshness refresh.
416
+ * @example
417
+ * ```ts
418
+ * declare const ctx: SageContext;
419
+ * const loaded = await preloadDefinitions(ctx, ['ships', 'claimStakes']);
420
+ * const provenance = readMeta(loaded);
421
+ * ```
422
+ */
423
+ export async function preloadDefinitions(context, sections, options = {}) {
290
424
  assertContextActive(context);
291
- const game = context.game;
292
- if (game === undefined)
293
- throw new MissingGameContextError();
294
- const state = stateFor(context);
295
- const existing = state.sections[section];
296
- if (existing !== undefined && !state.refresh.has(section))
297
- return existing;
298
- let loading = state.loading[section];
299
- if (loading === undefined) {
300
- loading = populateSection(context, game, section, state).finally(() => {
301
- delete state.loading[section];
302
- });
303
- state.loading[section] = loading;
425
+ options.signal?.throwIfAborted();
426
+ if (!Array.isArray(sections))
427
+ throw new InvalidDefinitionSectionError();
428
+ const targets = [];
429
+ for (let index = 0; index < sections.length; index += 1) {
430
+ const descriptor = Object.getOwnPropertyDescriptor(sections, index);
431
+ const section = descriptor !== undefined && 'value' in descriptor
432
+ ? descriptor.value
433
+ : undefined;
434
+ if (typeof section !== 'string' ||
435
+ ![
436
+ 'ships',
437
+ 'cargo',
438
+ 'buildings',
439
+ 'claimStakes',
440
+ 'craftingHabs',
441
+ 'habBuildings',
442
+ 'xp',
443
+ 'research',
444
+ ].includes(section))
445
+ throw new InvalidDefinitionSectionError();
446
+ const supported = section;
447
+ if (!targets.includes(supported))
448
+ targets.push(supported);
304
449
  }
305
- await loading;
306
- return state.sections[section];
450
+ if (targets.length > 0)
451
+ await waitForRegistry(context, ensureSections(context, targets), options.signal);
452
+ assertContextActive(context);
453
+ const state = stateFor(context);
454
+ const metadata = {
455
+ kind: 'definition-preload',
456
+ sections: targets.map((section) => {
457
+ const loaded = state.sections[section];
458
+ return {
459
+ section,
460
+ sequenceId: loaded.sequenceId,
461
+ observation: loaded.observation === undefined
462
+ ? { availability: 'unavailable' }
463
+ : { availability: 'available', ...loaded.observation },
464
+ };
465
+ }),
466
+ };
467
+ deepFreeze(metadata);
468
+ return Object.freeze(attachReadMeta(targets, metadata));
307
469
  }
308
470
  async function resolveDefinition(context, section, id, options) {
309
471
  const loaded = await ensureSection(context, section);
@@ -321,7 +483,7 @@ async function resolveDefinition(context, section, id, options) {
321
483
  performance.now() - confirmed.confirmedAtMonotonicMs >=
322
484
  SEQUENCE_MISMATCH_RETRY_DELAY_MS) {
323
485
  state.confirmingSequenceIds[section] = expected;
324
- state.refresh.add(section);
486
+ invalidateSection(state, section);
325
487
  }
326
488
  }
327
489
  throw new RegistryOutOfSyncError(section, expected, loaded.sequenceId, id);
@@ -330,7 +492,7 @@ async function resolveDefinition(context, section, id, options) {
330
492
  const definitionMissingAfterRefresh = state.confirmedMissingSequenceIds[section] === loaded.sequenceId;
331
493
  if (!definitionMissingAfterRefresh && !toleratesOlderCaller) {
332
494
  state.confirmingMissing.add(section);
333
- state.refresh.add(section);
495
+ invalidateSection(state, section);
334
496
  }
335
497
  throw new RegistryOutOfSyncError(section, expected, loaded.sequenceId, id, definitionMissingAfterRefresh);
336
498
  }
@@ -348,6 +510,71 @@ async function resolveDefinition(context, section, id, options) {
348
510
  export function resolveShip(context, shipId, options = {}) {
349
511
  return resolveDefinition(context, 'ships', shipId, options);
350
512
  }
513
+ /** Lists every ship configuration in the context's loaded Game definitions section.
514
+ *
515
+ * Entries and the readonly array are retained for the section generation. Each
516
+ * entry is the same immutable object returned by `resolveShip`; loading either
517
+ * route serves the other without another Game read. A section refresh replaces
518
+ * the array and its entries together. Existing returned arrays remain immutable
519
+ * snapshots of their prior generation.
520
+ *
521
+ * `definitionSequenceId` identifies the loaded section, not proven-current chain
522
+ * state. This enumerates existing configurations without grouping by model or
523
+ * filling unallocated ids. The result has no guaranteed sorting order.
524
+ *
525
+ * @example
526
+ * ```ts
527
+ * declare const ctx: SageContext;
528
+ * const configurations = await listShipConfigurations(ctx);
529
+ * for (const ship of configurations) {
530
+ * console.log(ship.id, ship.name, ship.stats.cargo.cargoCapacity);
531
+ * }
532
+ * ```
533
+ */
534
+ export async function listShipConfigurations(context) {
535
+ const loaded = (await ensureSection(context, 'ships'));
536
+ return loaded.configurations;
537
+ }
538
+ /** Lists current ship configurations associated with a mint in this context's Game.
539
+ *
540
+ * Groups by each configuration's on-chain `mint`, never display names, id ranges,
541
+ * or historical `mintToId` entries. Use a mint from the selected deployment;
542
+ * this does not map production mints to PTR or provide make/model labels or a
543
+ * default configuration. See `docs/research/ship-model-identity.md` (#395/#397).
544
+ *
545
+ * The frozen result shares objects with `resolveShip` and `listShipConfigurations`.
546
+ * A lazy index is retained with the ships section and replaced on section refresh;
547
+ * building it adds no RPC calls or metadata fetches. Unknown mints return a retained
548
+ * frozen empty array. Results have no guaranteed order or configuration count.
549
+ * `definitionSequenceId` denotes the loaded generation, not proven-current state.
550
+ *
551
+ * @example
552
+ * ```ts
553
+ * declare const ctx: SageContext;
554
+ * declare const shipMint: Address;
555
+ * const configurations = await listShipConfigurationsByMint(ctx, shipMint);
556
+ * for (const ship of configurations) {
557
+ * console.log(ship.id, ship.stats.cargo.cargoCapacity);
558
+ * }
559
+ * ```
560
+ */
561
+ export async function listShipConfigurationsByMint(context, mint) {
562
+ const loaded = (await ensureSection(context, 'ships'));
563
+ if (loaded.mintIndex === undefined) {
564
+ const groups = new Map();
565
+ for (const ship of loaded.configurations) {
566
+ const group = groups.get(ship.mint);
567
+ if (group === undefined)
568
+ groups.set(ship.mint, [ship]);
569
+ else
570
+ group.push(ship);
571
+ }
572
+ for (const group of groups.values())
573
+ Object.freeze(group);
574
+ loaded.mintIndex = { groups, empty: Object.freeze([]) };
575
+ }
576
+ return loaded.mintIndex.groups.get(mint) ?? loaded.mintIndex.empty;
577
+ }
351
578
  /** Resolves one cargo id to an immutable SDK-owned definition summary.
352
579
  *
353
580
  * @example
@@ -394,4 +621,48 @@ export async function listResearchDefinitions(context) {
394
621
  });
395
622
  }
396
623
  export { RegistryOutOfSyncError } from '../client/errors.js';
624
+ /** Internal Claim Stake lookup using the shared section lifecycle. */
625
+ export function resolveClaimStakeDefinition(context, id) {
626
+ return resolveDefinition(context, 'claimStakes', id, {});
627
+ }
628
+ /** Internal canonical Claim Stake catalog. */
629
+ export async function listClaimStakeDefinitions(context) {
630
+ return (await ensureSection(context, 'claimStakes')).catalog;
631
+ }
632
+ /** Internal canonical Crafting Hab container catalog. */
633
+ export async function listCraftingHabDefinitions(context) {
634
+ return (await ensureSection(context, 'craftingHabs')).catalog;
635
+ }
636
+ /** Internal Building definition lookup using the shared section lifecycle. */
637
+ export function resolveClaimStakeBuildingDefinition(context, id) {
638
+ return resolveDefinition(context, 'buildings', id, {});
639
+ }
640
+ /** Internal canonical Claim Stake building catalog. */
641
+ export async function listClaimStakeBuildingDefinitions(context) {
642
+ return (await ensureSection(context, 'buildings'))
643
+ .catalog;
644
+ }
645
+ /** Internal canonical Hab building catalog. */
646
+ export async function listHabBuildingDefinitions(context) {
647
+ return (await ensureSection(context, 'habBuildings')).catalog;
648
+ }
649
+ /** Lists all cargo definitions from the context's retained Game section.
650
+ *
651
+ * The readonly array and its definitions are shared with by-id lookups for one
652
+ * section generation. A refresh replaces the array and id map together; old
653
+ * arrays remain immutable snapshots. Results have no guaranteed sorting order.
654
+ * Loading is lazy and may use a compatible cargo schema-v1 stored snapshot.
655
+ * The retained generation does not prove latest-chain freshness.
656
+ *
657
+ * @example
658
+ * ```ts
659
+ * declare const ctx: SageContext;
660
+ * const definitions = await listCargoDefinitions(ctx);
661
+ * for (const cargo of definitions) console.log(cargo.id, cargo.name);
662
+ * ```
663
+ */
664
+ export async function listCargoDefinitions(context) {
665
+ const loaded = (await ensureSection(context, 'cargo'));
666
+ return (loaded.catalog ??= Object.freeze([...loaded.definitions.values()]));
667
+ }
397
668
  //# sourceMappingURL=index.js.map