@adea-ai/spatial 0.118.4 → 0.118.5

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.
@@ -51,9 +51,15 @@ export type SimFrameRequestMessage = Readonly<{
51
51
  }>;
52
52
  }> | Readonly<{
53
53
  type: 'frame.character.save';
54
+ /**
55
+ * `revision` orders saves from one frame: it increases with every save request the frame
56
+ * makes, so the host can refuse one older than a save it already committed. Optional so
57
+ * frames built before it keep working.
58
+ */
54
59
  payload: Readonly<{
55
60
  requestId: string;
56
61
  character: string;
62
+ revision?: number;
57
63
  }>;
58
64
  }>;
59
65
  /** The scene channel's acknowledgements, refusals and measurements (see `scene.ts`). */
@@ -136,11 +136,23 @@ export function parseFrameMessage(event, context) {
136
136
  : fail('invalid_payload');
137
137
  }
138
138
  case 'frame.character.save': {
139
- const body = strictRecord(payload, ['requestId', 'character']);
139
+ const body = strictRecord(payload, ['requestId', 'character'], ['revision']);
140
140
  const requestId = boundedString(body?.requestId, REQUEST_ID);
141
141
  const character = parseSimCharacterValue(body?.character);
142
+ const revision = body?.revision;
143
+ if (revision !== undefined &&
144
+ (typeof revision !== 'number' || !Number.isSafeInteger(revision) || revision < 1))
145
+ return fail('invalid_payload');
142
146
  return requestId && character
143
- ? { ok: true, message: { type, payload: { requestId, character } } }
147
+ ? {
148
+ ok: true,
149
+ message: {
150
+ type,
151
+ payload: revision === undefined
152
+ ? { requestId, character }
153
+ : { requestId, character, revision },
154
+ },
155
+ }
144
156
  : fail('invalid_payload');
145
157
  }
146
158
  case 'frame.scene.ack':
@@ -8,7 +8,10 @@ import type { SimFrameRequestMessage, SimHostMessage } from './messages';
8
8
  * `false`, so the engine never shows a saved value the host did not commit.
9
9
  */
10
10
  export type CharacterSaveClient = Readonly<{
11
- save(character: string): Promise<boolean>;
11
+ /** `revision` (a positive integer that increases per save) lets the host refuse an older save. */
12
+ save(character: string, options?: Readonly<{
13
+ revision?: number;
14
+ }>): Promise<boolean>;
12
15
  /** Feed every parsed host message in; unrelated ones are ignored. */
13
16
  receive(message: SimHostMessage): void;
14
17
  /** Fails any open save. Call when the frame is torn down. */
@@ -15,8 +15,12 @@ export function createCharacterSaveClient(options) {
15
15
  open?.settle(false);
16
16
  }
17
17
  return {
18
- save(character) {
19
- if (disposed || open || parseSimCharacterValue(character) === null)
18
+ save(character, saveOptions) {
19
+ const revision = saveOptions?.revision;
20
+ if (disposed ||
21
+ open ||
22
+ parseSimCharacterValue(character) === null ||
23
+ (revision !== undefined && (!Number.isSafeInteger(revision) || revision < 1)))
20
24
  return Promise.resolve(false);
21
25
  const requestId = mint();
22
26
  return new Promise((resolve) => {
@@ -33,7 +37,12 @@ export function createCharacterSaveClient(options) {
33
37
  };
34
38
  open = entry;
35
39
  try {
36
- options.post({ type: 'frame.character.save', payload: { requestId, character } });
40
+ options.post({
41
+ type: 'frame.character.save',
42
+ payload: revision === undefined
43
+ ? { requestId, character }
44
+ : { requestId, character, revision },
45
+ });
37
46
  }
38
47
  catch {
39
48
  entry.settle(false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/spatial",
3
- "version": "0.118.4",
3
+ "version": "0.118.5",
4
4
  "description": "Scene and asset manifest contracts plus the shell-to-engine spatial protocol for Adea workspaces: scene mount manifests, spawn URL helpers, and scene telemetry schemas. Pure types — no rendering, no binaries.",
5
5
  "keywords": [
6
6
  "assets",
@@ -57,7 +57,12 @@ export type SimFrameRequestMessage =
57
57
  }>
58
58
  | Readonly<{
59
59
  type: 'frame.character.save'
60
- payload: Readonly<{ requestId: string; character: string }>
60
+ /**
61
+ * `revision` orders saves from one frame: it increases with every save request the frame
62
+ * makes, so the host can refuse one older than a save it already committed. Optional so
63
+ * frames built before it keep working.
64
+ */
65
+ payload: Readonly<{ requestId: string; character: string; revision?: number }>
61
66
  }>
62
67
 
63
68
  /** The scene channel's acknowledgements, refusals and measurements (see `scene.ts`). */
@@ -204,11 +209,26 @@ export function parseFrameMessage(
204
209
  : fail('invalid_payload')
205
210
  }
206
211
  case 'frame.character.save': {
207
- const body = strictRecord(payload, ['requestId', 'character'])
212
+ const body = strictRecord(payload, ['requestId', 'character'], ['revision'])
208
213
  const requestId = boundedString(body?.requestId, REQUEST_ID)
209
214
  const character = parseSimCharacterValue(body?.character)
215
+ const revision = body?.revision
216
+ if (
217
+ revision !== undefined &&
218
+ (typeof revision !== 'number' || !Number.isSafeInteger(revision) || revision < 1)
219
+ )
220
+ return fail('invalid_payload')
210
221
  return requestId && character
211
- ? { ok: true, message: { type, payload: { requestId, character } } }
222
+ ? {
223
+ ok: true,
224
+ message: {
225
+ type,
226
+ payload:
227
+ revision === undefined
228
+ ? { requestId, character }
229
+ : { requestId, character, revision },
230
+ },
231
+ }
212
232
  : fail('invalid_payload')
213
233
  }
214
234
  case 'frame.scene.ack':
@@ -11,7 +11,8 @@ import type { SimFrameRequestMessage, SimHostMessage } from './messages'
11
11
  */
12
12
 
13
13
  export type CharacterSaveClient = Readonly<{
14
- save(character: string): Promise<boolean>
14
+ /** `revision` (a positive integer that increases per save) lets the host refuse an older save. */
15
+ save(character: string, options?: Readonly<{ revision?: number }>): Promise<boolean>
15
16
  /** Feed every parsed host message in; unrelated ones are ignored. */
16
17
  receive(message: SimHostMessage): void
17
18
  /** Fails any open save. Call when the frame is torn down. */
@@ -48,8 +49,14 @@ export function createCharacterSaveClient(
48
49
  }
49
50
 
50
51
  return {
51
- save(character) {
52
- if (disposed || open || parseSimCharacterValue(character) === null)
52
+ save(character, saveOptions) {
53
+ const revision = saveOptions?.revision
54
+ if (
55
+ disposed ||
56
+ open ||
57
+ parseSimCharacterValue(character) === null ||
58
+ (revision !== undefined && (!Number.isSafeInteger(revision) || revision < 1))
59
+ )
53
60
  return Promise.resolve(false)
54
61
  const requestId = mint()
55
62
  return new Promise<boolean>((resolve) => {
@@ -65,7 +72,13 @@ export function createCharacterSaveClient(
65
72
  }
66
73
  open = entry
67
74
  try {
68
- options.post({ type: 'frame.character.save', payload: { requestId, character } })
75
+ options.post({
76
+ type: 'frame.character.save',
77
+ payload:
78
+ revision === undefined
79
+ ? { requestId, character }
80
+ : { requestId, character, revision },
81
+ })
69
82
  } catch {
70
83
  entry.settle(false)
71
84
  }