@things-factory/figure-ui 10.1.110 → 10.1.112

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.110",
3
+ "version": "10.1.112",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -27,7 +27,7 @@
27
27
  "clean": "npm run clean:server && npm run clean:client"
28
28
  },
29
29
  "dependencies": {
30
- "@hatiolab/figure-model": "^0.2.8",
30
+ "@hatiolab/figure-model": "^0.2.10",
31
31
  "@hatiolab/things-scene": "^10.1.65",
32
32
  "@material/web": "^2.0.0",
33
33
  "@operato/data-grist": "^10.0.0",
@@ -40,9 +40,9 @@
40
40
  "@operato/shell": "^10.0.0",
41
41
  "@operato/styles": "^10.0.0",
42
42
  "@operato/utils": "^10.0.0",
43
- "@things-factory/ai-assistant": "^10.1.110",
44
- "@things-factory/figure-service": "^10.1.110",
43
+ "@things-factory/ai-assistant": "^10.1.112",
44
+ "@things-factory/figure-service": "^10.1.112",
45
45
  "three": "^0.185.1"
46
46
  },
47
- "gitHead": "b68feb6eccbef44af2a6365f384bd852dd70a8f9"
47
+ "gitHead": "e300d1b60b98ccb80380afe8df6d2807b1bcff91"
48
48
  }
@@ -10,8 +10,11 @@ import { fileURLToPath } from 'node:url'
10
10
  import { addFigureBox, createFigureAsset, serializeFigureAsset } from '@hatiolab/figure-model'
11
11
 
12
12
  import { createFigureProductSession } from '../client/modeller/product-session.js'
13
+ import { thumbnailHashOf } from '../client/modeller/thumbnail-hash.js'
13
14
 
14
15
  const read = (path: string) => readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8')
16
+ /** A picture as the modeller sends one: a data URL. */
17
+ const pic = (word: string) => `data:image/webp;base64,${Buffer.from(word).toString('base64')}`
15
18
  const unreachable = () => Object.assign(Error('서버에 연결하지 못했습니다.'), { code: 'SAVE_UNREACHABLE' })
16
19
 
17
20
  /** A server that keeps one row; its save can be made to fail before or after it writes. */
@@ -23,7 +26,8 @@ function server() {
23
26
  load: async (id: string) => ({ id, source: row }),
24
27
  readBack: async (id: string) => {
25
28
  if (state.loadFails) throw unreachable()
26
- return { id, source: row, thumbnail: picture }
29
+ /* The server hands back the name it keeps the picture under, never the bytes. */
30
+ return { id, source: row, thumbnailHash: picture === undefined ? undefined : await thumbnailHashOf(picture) }
27
31
  },
28
32
  save: async (q: { id?: string; source: string; expectedSource?: string; thumbnail?: string }) => {
29
33
  if (state.hold) await state.hold
@@ -131,9 +135,9 @@ test('a refusal on grounds is not a save, even if the row holds the same text; a
131
135
  await other.open('a')
132
136
  other.command({ kind: 'rename', name: 'B' } as never)
133
137
  s2.setRow(serializeFigureAsset(JSON.parse(other.source())))
134
- s2.setPicture('old-picture')
138
+ s2.setPicture(pic('old-picture'))
135
139
  s2.state.failBeforeWrite = true
136
- await assert.rejects(other.save({ thumbnail: 'new-picture' }), (e: { code?: string }) => e.code === 'SAVE_UNREACHABLE')
140
+ await assert.rejects(other.save({ thumbnail: pic('new-picture') }), (e: { code?: string }) => e.code === 'SAVE_UNREACHABLE')
137
141
  assert.equal(other.read().dirty, true)
138
142
  })
139
143
 
@@ -187,12 +191,24 @@ test('the sent content confirmed on the server means every field of it, the pict
187
191
  session.command({ kind: 'rename', name: 'B' } as never)
188
192
  s.state.failAfterWrite = true
189
193
  s.state.loadFails = true
190
- await assert.rejects(session.save({ thumbnail: 'mine' }), (e: { code?: string }) => e.code === 'SAVE_UNREACHABLE')
194
+ await assert.rejects(session.save({ thumbnail: pic('mine') }), (e: { code?: string }) => e.code === 'SAVE_UNREACHABLE')
191
195
  s.state.failAfterWrite = false
192
196
  s.state.loadFails = false
193
197
  /* Someone else changed the picture since: the source matches what was sent, the picture does not. */
194
- s.setPicture('theirs')
198
+ s.setPicture(pic('theirs'))
195
199
  session.command({ kind: 'rename', name: 'C' } as never)
196
- await assert.rejects(session.save({ thumbnail: 'mine-2' }), /다른 편집에서/)
200
+ await assert.rejects(session.save({ thumbnail: pic('mine-2') }), /다른 편집에서/)
197
201
  assert.equal(session.read().dirty, true)
198
202
  })
203
+
204
+ /* ADR-0104 decision 6: the server keeps the picture by the hash of its bytes; read back, the sent picture is matched by it. */
205
+ test('the answer lost after the picture was written: read back by its hash, it was saved', async () => {
206
+ const s = server()
207
+ const session = createFigureProductSession(s.store)
208
+ await session.open('a')
209
+ session.command({ kind: 'rename', name: 'B' } as never)
210
+ s.state.failAfterWrite = true
211
+ await session.save({ thumbnail: pic('card') })
212
+ assert.equal(session.read().dirty, false, 'source and picture both landed')
213
+ assert.equal(await thumbnailHashOf('not a picture'), undefined)
214
+ })