@speckle/objectsender 1.0.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.
@@ -0,0 +1,61 @@
1
+ import { ITransport } from './ITransport'
2
+ import { IDisposable } from '../utils/IDisposable'
3
+ /**
4
+ * Basic object sender to a speckle server
5
+ */
6
+ export class ServerTransport implements ITransport, IDisposable {
7
+ buffer: string[]
8
+ maxSize: number
9
+ currSize: number
10
+ serverUrl: string
11
+ projectId: string
12
+ authToken: string
13
+
14
+ constructor(
15
+ serverUrl: string,
16
+ projectId: string,
17
+ authToken: string,
18
+ maxSize: number = 200_000
19
+ ) {
20
+ this.maxSize = maxSize
21
+ this.currSize = 0
22
+ this.serverUrl = serverUrl
23
+ this.projectId = projectId
24
+ this.authToken = authToken
25
+ this.buffer = []
26
+ }
27
+
28
+ async write(serialisedObject: string, size: number) {
29
+ this.buffer.push(serialisedObject)
30
+ this.currSize += size
31
+ if (this.currSize < this.maxSize) return // return fast
32
+ await this.flush() // block until we send objects
33
+ }
34
+
35
+ async flush() {
36
+ if (this.buffer.length === 0) return
37
+
38
+ const formData = new FormData()
39
+ const concat = '[' + this.buffer.join(',') + ']'
40
+ formData.append('object-batch', new Blob([concat], { type: 'application/json' }))
41
+ const url = new URL(`/objects/${this.projectId}`, this.serverUrl)
42
+ const res = await fetch(url, {
43
+ method: 'POST',
44
+ headers: { Authorization: `Bearer ${this.authToken}` },
45
+ body: formData
46
+ })
47
+
48
+ if (res.status !== 201) {
49
+ throw new Error(
50
+ `Unexpected error when sending data. Expected status 200, got ${res.status}`
51
+ )
52
+ }
53
+
54
+ this.buffer = []
55
+ this.currSize = 0
56
+ }
57
+
58
+ dispose() {
59
+ this.buffer = []
60
+ }
61
+ }
@@ -0,0 +1,15 @@
1
+ /* eslint-disable camelcase */
2
+ /**
3
+ * Basic 'Base'-like object from .NET. It will create a 'speckle_type' prop that defaults to the class' name. This can be overriden by providing yourself a 'speckle_type' property in the props argument of the constructor.
4
+ */
5
+ export class Base implements Record<string, unknown> {
6
+ speckle_type: string
7
+ constructor(props?: Record<string, unknown>) {
8
+ this.speckle_type = this.constructor.name
9
+
10
+ if (props) {
11
+ for (const key in props) this[key] = props[key]
12
+ }
13
+ }
14
+ [x: string]: unknown
15
+ }
@@ -0,0 +1,3 @@
1
+ export interface IDisposable {
2
+ dispose: () => void
3
+ }
@@ -0,0 +1,248 @@
1
+ /* eslint-disable camelcase */
2
+ import { SHA1 } from './Sha1'
3
+ import { ITransport } from '../transports/ITransport'
4
+ import { Base } from './Base'
5
+ import { IDisposable } from './IDisposable'
6
+ import { isObjectLike, get } from '#lodash'
7
+
8
+ type BasicSpeckleObject = Record<string, unknown> & {
9
+ speckle_type: string
10
+ }
11
+
12
+ const isSpeckleObject = (obj: unknown): obj is BasicSpeckleObject =>
13
+ isObjectLike(obj) && !!get(obj, 'speckle_type')
14
+
15
+ export class Serializer implements IDisposable {
16
+ chunkSize: number
17
+ detachLineage: boolean[]
18
+ lineage: string[]
19
+ familyTree: Record<string, Record<string, number>>
20
+ closureTable: Record<string, unknown>
21
+ transport: ITransport | null
22
+ uniqueId: number
23
+ hashingFunction: (s: string) => string
24
+
25
+ constructor(
26
+ transport: ITransport,
27
+ chunkSize: number = 1000,
28
+ hashingFunction: (s: string) => string = SHA1
29
+ ) {
30
+ this.chunkSize = chunkSize
31
+ this.detachLineage = [true] // first ever call is always detached
32
+ this.lineage = []
33
+ this.familyTree = {}
34
+ this.closureTable = {}
35
+ this.transport = transport
36
+ this.uniqueId = 0
37
+ this.hashingFunction = hashingFunction || SHA1
38
+ }
39
+
40
+ async write(obj: Base) {
41
+ return await this.#traverse(obj, true)
42
+ }
43
+
44
+ async #traverse(obj: Record<string, unknown>, root: boolean) {
45
+ const temporaryId = `${this.uniqueId++}-obj`
46
+ this.lineage.push(temporaryId)
47
+
48
+ const traversed = { speckle_type: obj.speckle_type || 'Base' } as Record<
49
+ string,
50
+ unknown
51
+ >
52
+
53
+ for (const propKey in obj) {
54
+ const value = obj[propKey]
55
+ // 0. skip some props
56
+ if (!value || propKey === 'id' || propKey.startsWith('_')) continue
57
+
58
+ // 1. primitives (numbers, bools, strings)
59
+ if (typeof value !== 'object') {
60
+ traversed[propKey] = value
61
+ continue
62
+ }
63
+
64
+ const isDetachedProp = propKey.startsWith('@')
65
+
66
+ // 2. chunked arrays
67
+ const isArray = Array.isArray(value)
68
+ const isChunked = isArray ? propKey.match(/^@\((\d*)\)/) : false // chunk syntax
69
+ if (isArray && isChunked && value.length !== 0 && typeof value[0] !== 'object') {
70
+ const chunkSize = isChunked[1] !== '' ? parseInt(isChunked[1]) : this.chunkSize
71
+ const chunkRefs = []
72
+
73
+ let chunk = new DataChunk()
74
+ let count = 0
75
+ for (const el of value) {
76
+ if (count === chunkSize) {
77
+ chunkRefs.push(await this.#handleChunk(chunk))
78
+ chunk = new DataChunk()
79
+ count = 0
80
+ }
81
+ chunk.data.push(el)
82
+ count++
83
+ }
84
+
85
+ if (chunk.data.length !== 0) chunkRefs.push(await this.#handleChunk(chunk))
86
+ traversed[propKey.replace(isChunked[0], '')] = chunkRefs // strip chunk syntax
87
+ continue
88
+ }
89
+
90
+ // 3. speckle objects
91
+ if ((value as Record<string, unknown>).speckle_type) {
92
+ const child = (await this.#traverseValue({
93
+ value,
94
+ isDetached: isDetachedProp
95
+ })) as {
96
+ id: string
97
+ }
98
+ traversed[propKey] = isDetachedProp ? this.#detachHelper(child.id) : child
99
+ continue
100
+ }
101
+
102
+ // 4. other objects (dicts/maps, lists)
103
+ traversed[propKey] = await this.#traverseValue({
104
+ value,
105
+ isDetached: isDetachedProp
106
+ })
107
+ }
108
+ // We've finished going through all the properties of this object, now let's perform the last rites
109
+ const detached = this.detachLineage.pop()
110
+ const parent = this.lineage.pop() as string
111
+
112
+ if (this.familyTree[parent]) {
113
+ const closure = {} as Record<string, number>
114
+
115
+ Object.entries(this.familyTree[parent]).forEach(([ref, depth]) => {
116
+ closure[ref] = depth - this.detachLineage.length
117
+ })
118
+
119
+ traversed['totalChildrenCount'] = Object.keys(closure).length
120
+
121
+ if (traversed['totalChildrenCount']) {
122
+ traversed['__closure'] = closure
123
+ }
124
+ }
125
+
126
+ const { hash, serializedObject, size } = this.#generateId(traversed)
127
+ traversed.id = hash
128
+
129
+ // Pop it in
130
+ if ((detached || root) && this.transport) {
131
+ await this.transport.write(serializedObject, size)
132
+ }
133
+
134
+ // We've reached the end, let's flush
135
+ if (root && this.transport) {
136
+ await this.transport.flush()
137
+ }
138
+
139
+ return { hash, traversed }
140
+ }
141
+
142
+ async #traverseValue({
143
+ value,
144
+ isDetached = false
145
+ }: {
146
+ value: unknown
147
+ isDetached?: boolean
148
+ }): Promise<unknown> {
149
+ // 1. primitives
150
+ if (typeof value !== 'object') return value
151
+
152
+ // 2. arrays
153
+ if (Array.isArray(value)) {
154
+ const arr = value as unknown[]
155
+ // 2.1 empty arrays
156
+ if (arr.length === 0) return value as unknown
157
+
158
+ // 2.2 primitive arrays
159
+ if (typeof arr[0] !== 'object') return arr
160
+
161
+ // 2.3. non-primitive non-detached arrays
162
+ if (!isDetached) {
163
+ return Promise.all(
164
+ value.map(async (el) => await this.#traverseValue({ value: el }))
165
+ )
166
+ }
167
+
168
+ // 2.4 non-primitive detached arrays
169
+ const detachedList = [] as unknown[]
170
+ for (const el of value) {
171
+ if (isSpeckleObject(el)) {
172
+ this.detachLineage.push(isDetached)
173
+ const { hash } = await this.#traverse(el, false)
174
+ detachedList.push(this.#detachHelper(hash))
175
+ } else {
176
+ detachedList.push(await this.#traverseValue({ value: el, isDetached }))
177
+ }
178
+ }
179
+ return detachedList
180
+ }
181
+
182
+ // 3. dicts
183
+ if (!(value as { speckle_type?: string }).speckle_type) return value
184
+
185
+ // 4. base objects
186
+ if ((value as { speckle_type?: string }).speckle_type) {
187
+ this.detachLineage.push(isDetached)
188
+ const res = await this.#traverse(value as Record<string, unknown>, false)
189
+ return res.traversed
190
+ }
191
+
192
+ throw new Error(`Unsupported type '${typeof value}': ${value}.`)
193
+ }
194
+
195
+ #detachHelper(refHash: string) {
196
+ this.lineage.forEach((parent) => {
197
+ if (!this.familyTree[parent]) this.familyTree[parent] = {}
198
+
199
+ if (
200
+ !this.familyTree[parent][refHash] ||
201
+ this.familyTree[parent][refHash] > this.detachLineage.length
202
+ ) {
203
+ this.familyTree[parent][refHash] = this.detachLineage.length
204
+ }
205
+ })
206
+ return {
207
+ referencedId: refHash,
208
+ speckle_type: 'reference'
209
+ }
210
+ }
211
+
212
+ async #handleChunk(chunk: DataChunk) {
213
+ this.detachLineage.push(true)
214
+ const { hash } = await this.#traverse(
215
+ chunk as unknown as Record<string, unknown>,
216
+ false
217
+ )
218
+ return this.#detachHelper(hash)
219
+ }
220
+
221
+ #generateId(obj: Record<string, unknown>) {
222
+ const s = JSON.stringify(obj)
223
+ const h = this.hashingFunction(s)
224
+ const f = s.substring(0, 1) + `"id":"${h}",` + s.substring(1)
225
+ return {
226
+ hash: SHA1(s),
227
+ serializedObject: f,
228
+ size: s.length // approx, good enough as we're just limiting artificially batch sizes based on this
229
+ }
230
+ }
231
+
232
+ dispose() {
233
+ this.detachLineage = []
234
+ this.lineage = []
235
+ this.familyTree = {}
236
+ this.closureTable = {}
237
+ this.transport = null
238
+ }
239
+ }
240
+
241
+ class DataChunk {
242
+ speckle_type: 'Speckle.Core.Models.DataChunk'
243
+ data: unknown[]
244
+ constructor() {
245
+ this.data = []
246
+ this.speckle_type = 'Speckle.Core.Models.DataChunk'
247
+ }
248
+ }
@@ -0,0 +1,138 @@
1
+ /* eslint-disable camelcase */
2
+ /**
3
+ * Basic hashing function, to avoid dependencies and crazy build processes
4
+ * @param msg
5
+ * @returns
6
+ */
7
+ export function SHA1(msg: string) {
8
+ function rotate_left(n: number, s: number) {
9
+ const t4 = (n << s) | (n >>> (32 - s))
10
+ return t4
11
+ }
12
+ function cvt_hex(val: number) {
13
+ let str = ''
14
+ let i
15
+ let v
16
+ for (i = 7; i >= 0; i--) {
17
+ v = (val >>> (i * 4)) & 0x0f
18
+ str += v.toString(16)
19
+ }
20
+ return str
21
+ }
22
+ function Utf8Encode(string: string) {
23
+ string = string.replace(/\r\n/g, '\n')
24
+ let utftext = ''
25
+ for (let n = 0; n < string.length; n++) {
26
+ const c = string.charCodeAt(n)
27
+ if (c < 128) {
28
+ utftext += String.fromCharCode(c)
29
+ } else if (c > 127 && c < 2048) {
30
+ utftext += String.fromCharCode((c >> 6) | 192)
31
+ utftext += String.fromCharCode((c & 63) | 128)
32
+ } else {
33
+ utftext += String.fromCharCode((c >> 12) | 224)
34
+ utftext += String.fromCharCode(((c >> 6) & 63) | 128)
35
+ utftext += String.fromCharCode((c & 63) | 128)
36
+ }
37
+ }
38
+ return utftext
39
+ }
40
+ let blockstart
41
+ let i, j
42
+ const W = new Array(80)
43
+ let H0 = 0x67452301
44
+ let H1 = 0xefcdab89
45
+ let H2 = 0x98badcfe
46
+ let H3 = 0x10325476
47
+ let H4 = 0xc3d2e1f0
48
+ let A, B, C, D, E
49
+ let temp
50
+ msg = Utf8Encode(msg)
51
+ const msg_len = msg.length
52
+ const word_array = [] as unknown[]
53
+ for (i = 0; i < msg_len - 3; i += 4) {
54
+ j =
55
+ (msg.charCodeAt(i) << 24) |
56
+ (msg.charCodeAt(i + 1) << 16) |
57
+ (msg.charCodeAt(i + 2) << 8) |
58
+ msg.charCodeAt(i + 3)
59
+ word_array.push(j)
60
+ }
61
+ switch (msg_len % 4) {
62
+ case 0:
63
+ i = 0x080000000
64
+ break
65
+ case 1:
66
+ i = (msg.charCodeAt(msg_len - 1) << 24) | 0x0800000
67
+ break
68
+ case 2:
69
+ i =
70
+ (msg.charCodeAt(msg_len - 2) << 24) |
71
+ (msg.charCodeAt(msg_len - 1) << 16) |
72
+ 0x08000
73
+ break
74
+ case 3:
75
+ i =
76
+ (msg.charCodeAt(msg_len - 3) << 24) |
77
+ (msg.charCodeAt(msg_len - 2) << 16) |
78
+ (msg.charCodeAt(msg_len - 1) << 8) |
79
+ 0x80
80
+ break
81
+ }
82
+ word_array.push(i)
83
+ while (word_array.length % 16 !== 14) word_array.push(0)
84
+ word_array.push(msg_len >>> 29)
85
+ word_array.push((msg_len << 3) & 0x0ffffffff)
86
+ for (blockstart = 0; blockstart < word_array.length; blockstart += 16) {
87
+ for (i = 0; i < 16; i++) W[i] = word_array[blockstart + i]
88
+ for (i = 16; i <= 79; i++)
89
+ W[i] = rotate_left(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1)
90
+ A = H0
91
+ B = H1
92
+ C = H2
93
+ D = H3
94
+ E = H4
95
+ for (i = 0; i <= 19; i++) {
96
+ temp =
97
+ (rotate_left(A, 5) + ((B & C) | (~B & D)) + E + W[i] + 0x5a827999) & 0x0ffffffff
98
+ E = D
99
+ D = C
100
+ C = rotate_left(B, 30)
101
+ B = A
102
+ A = temp
103
+ }
104
+ for (i = 20; i <= 39; i++) {
105
+ temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0x6ed9eba1) & 0x0ffffffff
106
+ E = D
107
+ D = C
108
+ C = rotate_left(B, 30)
109
+ B = A
110
+ A = temp
111
+ }
112
+ for (i = 40; i <= 59; i++) {
113
+ temp =
114
+ (rotate_left(A, 5) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8f1bbcdc) &
115
+ 0x0ffffffff
116
+ E = D
117
+ D = C
118
+ C = rotate_left(B, 30)
119
+ B = A
120
+ A = temp
121
+ }
122
+ for (i = 60; i <= 79; i++) {
123
+ temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0xca62c1d6) & 0x0ffffffff
124
+ E = D
125
+ D = C
126
+ C = rotate_left(B, 30)
127
+ B = A
128
+ A = temp
129
+ }
130
+ H0 = (H0 + A) & 0x0ffffffff
131
+ H1 = (H1 + B) & 0x0ffffffff
132
+ H2 = (H2 + C) & 0x0ffffffff
133
+ H3 = (H3 + D) & 0x0ffffffff
134
+ H4 = (H4 + E) & 0x0ffffffff
135
+ }
136
+ const h = cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) + cvt_hex(H4)
137
+ return h.toLowerCase().substring(0, 32)
138
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "noFallthroughCasesInSwitch": true
21
+ },
22
+ "include": ["src"]
23
+ }
package/vite.config.js ADDED
@@ -0,0 +1,18 @@
1
+ import pkg from './package.json'
2
+ import { defineConfig } from 'vite'
3
+ import { resolve } from 'path'
4
+
5
+ export default defineConfig({
6
+ build: {
7
+ lib: {
8
+ entry: resolve(import.meta.dirname, './src/index.ts'),
9
+ name: 'objectsender',
10
+ fileName: 'objectsender',
11
+ formats: ['es', 'cjs']
12
+ },
13
+ sourcemap: true,
14
+ rollupOptions: {
15
+ external: Object.keys(pkg.dependencies || {})
16
+ }
17
+ }
18
+ })