@shipload/sdk 1.0.0-next.61 → 1.0.0-next.62

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipload/sdk",
3
3
  "description": "SDKs for Shipload",
4
- "version": "1.0.0-next.61",
4
+ "version": "1.0.0-next.62",
5
5
  "homepage": "https://github.com/shipload/toolkit/tree/master/packages/sdk",
6
6
  "repository": {
7
7
  "type": "git",
@@ -51,6 +51,7 @@ export {
51
51
  computeFreeCells,
52
52
  NftManager,
53
53
  ConstructionManager,
54
+ JobsManager,
54
55
  } from './managers'
55
56
  export type {GridCell, ClusterCell, Cluster} from './managers'
56
57
  export type {
@@ -288,8 +289,15 @@ export type {
288
289
  export {taskCargoChanges} from './scheduling/task-cargo'
289
290
  export type {TaskCargoChange, TaskCargoDirection} from './scheduling/task-cargo'
290
291
 
291
- export {jobsToLanes, pickFabricator, socketTail, JOB_QUEUE_CAP} from './scheduling/jobs'
292
- export type {JobWindow, JobLane, JobLaneEntry} from './scheduling/jobs'
292
+ export {
293
+ jobsToLanes,
294
+ pickFabricator,
295
+ socketTail,
296
+ JOB_QUEUE_CAP,
297
+ jobStatus,
298
+ splitJobCargo,
299
+ } from './scheduling/jobs'
300
+ export type {JobWindow, JobLane, JobLaneEntry, JobStatus, OwnedJob} from './scheduling/jobs'
293
301
 
294
302
  export {composeIdleResolve} from './scheduling/idle-resolve'
295
303
  export type {CounterpartLookup, IdleResolveTarget} from './scheduling/idle-resolve'
@@ -11,6 +11,7 @@ import {EpochsManager} from './epochs'
11
11
  import {ActionsManager} from './actions'
12
12
  import {ClusterManager} from './cluster'
13
13
  import {NftManager} from './nft'
14
+ import {JobsManager} from './jobs'
14
15
  import {SubscriptionsManager} from '../subscriptions/manager'
15
16
 
16
17
  export class GameContext {
@@ -22,6 +23,7 @@ export class GameContext {
22
23
  private _actions?: ActionsManager
23
24
  private _clusters?: ClusterManager
24
25
  private _nft?: NftManager
26
+ private _jobs?: JobsManager
25
27
  private _subscriptions?: SubscriptionsManager
26
28
  private _subscriptionsUrl?: string
27
29
 
@@ -91,6 +93,13 @@ export class GameContext {
91
93
  return this._nft
92
94
  }
93
95
 
96
+ get jobs(): JobsManager {
97
+ if (!this._jobs) {
98
+ this._jobs = new JobsManager(this)
99
+ }
100
+ return this._jobs
101
+ }
102
+
94
103
  setSubscriptionsUrl(url: string) {
95
104
  this._subscriptionsUrl = url
96
105
  }
@@ -12,6 +12,7 @@ export {ClusterManager, computeFreeCells} from './cluster'
12
12
  export type {GridCell, ClusterCell, Cluster} from './cluster'
13
13
  export {NftManager} from './nft'
14
14
  export type {NftConfigForItem} from './nft'
15
+ export {JobsManager} from './jobs'
15
16
  export {PlotManager} from './plot'
16
17
  export type {PlotProgress, PlotProgressInputRow} from './plot'
17
18
  export {ConstructionManager} from './construction'
@@ -0,0 +1,62 @@
1
+ import {describe, expect, it} from 'bun:test'
2
+ import {Name} from '@wharfkit/antelope'
3
+ import {JobsManager} from './jobs'
4
+
5
+ const OWNER = 'eggmaple.gm'
6
+ const row = (over: Record<string, unknown> = {}) => ({
7
+ id: {toNumber: () => 7},
8
+ workshop: {toNumber: () => 42},
9
+ socket: {toNumber: () => 0},
10
+ owner: Name.from(OWNER),
11
+ ship_id: {toNumber: () => 100},
12
+ coords: {x: {toNumber: () => 12}, y: {toNumber: () => 34}},
13
+ starts_at: {toDate: () => new Date('2026-07-26T10:00:00Z')},
14
+ completes_at: {toDate: () => new Date('2026-07-26T11:00:00Z')},
15
+ recipe_id: {toNumber: () => 10001},
16
+ quantity: {toNumber: () => 5},
17
+ energy_paid: {toNumber: () => 0},
18
+ cargo: [{item: 'in'}, {item: 'out'}],
19
+ ...over,
20
+ })
21
+
22
+ function managerWith(queryImpl: () => Promise<unknown[]>, allImpl: () => Promise<unknown[]>) {
23
+ const table = () => ({query: () => ({all: queryImpl}), all: allImpl})
24
+ const ctx = {server: {table}} as never
25
+ return new JobsManager(ctx)
26
+ }
27
+
28
+ describe('JobsManager.getOwnedJobs', () => {
29
+ it('parses owner rows: output = last cargo, inputs = rest, status derived', async () => {
30
+ const m = managerWith(
31
+ async () => [row()],
32
+ async () => []
33
+ )
34
+ const jobs = await m.getOwnedJobs(OWNER, {now: new Date('2026-07-26T11:30:00Z')})
35
+ expect(jobs).toHaveLength(1)
36
+ expect(jobs[0]).toMatchObject({id: 7, workshop: 42, quantity: 5, status: 'ready'})
37
+ expect(jobs[0].coords).toEqual({x: 12, y: 34})
38
+ expect(jobs[0].output).toEqual({item: 'out'} as never)
39
+ expect(jobs[0].inputs).toEqual([{item: 'in'}] as never)
40
+ })
41
+
42
+ it('drops rows whose owner does not match (positional-index safety re-filter)', async () => {
43
+ const other = row({owner: Name.from('someoneelse.gm')})
44
+ const m = managerWith(
45
+ async () => [row(), other],
46
+ async () => []
47
+ )
48
+ const jobs = await m.getOwnedJobs(OWNER)
49
+ expect(jobs).toHaveLength(1)
50
+ })
51
+
52
+ it('falls back to a full-scan .all() when the positional query throws', async () => {
53
+ const m = managerWith(
54
+ async () => {
55
+ throw new Error('bad index_position')
56
+ },
57
+ async () => [row()]
58
+ )
59
+ const jobs = await m.getOwnedJobs(OWNER)
60
+ expect(jobs).toHaveLength(1)
61
+ })
62
+ })
@@ -0,0 +1,52 @@
1
+ import {Name, type NameType} from '@wharfkit/antelope'
2
+ import {BaseManager} from './base'
3
+ import {jobStatus, splitJobCargo, type OwnedJob} from '../scheduling/jobs'
4
+ import type {ServerContract} from '../contracts'
5
+
6
+ type JobRow = ServerContract.Types.job_row
7
+
8
+ export class JobsManager extends BaseManager {
9
+ async getOwnedJobs(owner: NameType, opts?: {now?: Date}): Promise<OwnedJob[]> {
10
+ const ownerName = Name.from(owner)
11
+ const now = opts?.now ?? new Date()
12
+
13
+ let rows: JobRow[]
14
+ try {
15
+ // index_position 'tertiary' = nodeos slot 3 = owner's secondary index; no ABI metadata for it.
16
+ rows = (await this.server
17
+ .table('jobs')
18
+ .query({
19
+ index_position: 'tertiary',
20
+ key_type: 'i64',
21
+ from: ownerName.value,
22
+ to: ownerName.value,
23
+ })
24
+ .all()) as JobRow[]
25
+ } catch {
26
+ rows = (await this.server.table('jobs').all()) as JobRow[]
27
+ }
28
+
29
+ return rows.filter((r) => ownerName.equals(r.owner)).map((r) => this.parseOwnedJob(r, now))
30
+ }
31
+
32
+ private parseOwnedJob(r: JobRow, now: Date): OwnedJob {
33
+ const startsAt = r.starts_at.toDate()
34
+ const completesAt = r.completes_at.toDate()
35
+ const {output, inputs} = splitJobCargo(r.cargo)
36
+ return {
37
+ id: r.id.toNumber(),
38
+ workshop: r.workshop.toNumber(),
39
+ socket: r.socket.toNumber(),
40
+ shipId: r.ship_id.toNumber(),
41
+ coords: {x: r.coords.x.toNumber(), y: r.coords.y.toNumber()},
42
+ startsAt,
43
+ completesAt,
44
+ recipeId: r.recipe_id.toNumber(),
45
+ quantity: r.quantity.toNumber(),
46
+ status: jobStatus({startsAt, completesAt}, now),
47
+ output,
48
+ inputs,
49
+ outputStats: output?.stats === undefined ? undefined : BigInt(output.stats.toString()),
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,32 @@
1
+ import {describe, expect, it} from 'bun:test'
2
+ import {jobStatus, splitJobCargo} from './jobs'
3
+
4
+ const at = (s: string) => new Date(s)
5
+
6
+ describe('jobStatus', () => {
7
+ const job = {startsAt: at('2026-07-26T10:00:00Z'), completesAt: at('2026-07-26T11:00:00Z')}
8
+ it('is waiting before startsAt', () => {
9
+ expect(jobStatus(job, at('2026-07-26T09:59:59Z'))).toBe('waiting')
10
+ })
11
+ it('is crafting between startsAt and completesAt', () => {
12
+ expect(jobStatus(job, at('2026-07-26T10:30:00Z'))).toBe('crafting')
13
+ })
14
+ it('is ready at or after completesAt', () => {
15
+ expect(jobStatus(job, at('2026-07-26T11:00:00Z'))).toBe('ready')
16
+ expect(jobStatus(job, at('2026-07-26T12:00:00Z'))).toBe('ready')
17
+ })
18
+ })
19
+
20
+ describe('splitJobCargo', () => {
21
+ it('takes the last element as output, the rest as inputs', () => {
22
+ const cargo = [{n: 'a'}, {n: 'b'}, {n: 'out'}] as unknown as Parameters<
23
+ typeof splitJobCargo
24
+ >[0]
25
+ const {output, inputs} = splitJobCargo(cargo)
26
+ expect(output).toEqual({n: 'out'} as never)
27
+ expect(inputs).toEqual([{n: 'a'}, {n: 'b'}] as never)
28
+ })
29
+ it('returns null output for empty cargo', () => {
30
+ expect(splitJobCargo([])).toEqual({output: null, inputs: []})
31
+ })
32
+ })
@@ -1,3 +1,7 @@
1
+ import type {ServerContract} from '../contracts'
2
+
3
+ type CargoItem = ServerContract.Types.cargo_item
4
+
1
5
  export interface JobWindow {
2
6
  id: number
3
7
  socket: number
@@ -6,6 +10,8 @@ export interface JobWindow {
6
10
  completesAt: Date
7
11
  recipeId: number
8
12
  quantity: number
13
+ /** Packed stat roll of the job's output, when the source carried the job's cargo. */
14
+ outputStats?: bigint
9
15
  }
10
16
 
11
17
  export interface JobLaneEntry {
@@ -69,3 +75,34 @@ export function pickFabricator(
69
75
  }
70
76
  return best
71
77
  }
78
+
79
+ export type JobStatus = 'waiting' | 'crafting' | 'ready'
80
+
81
+ export function jobStatus(job: {startsAt: Date; completesAt: Date}, now: Date): JobStatus {
82
+ if (now < job.startsAt) return 'waiting'
83
+ if (now < job.completesAt) return 'crafting'
84
+ return 'ready'
85
+ }
86
+
87
+ // Generic in the element so raw chain JSON (readonly actions) splits by the same rule as decoded rows.
88
+ export function splitJobCargo<T>(cargo: readonly T[]): {output: T | null; inputs: T[]} {
89
+ if (cargo.length === 0) return {output: null, inputs: []}
90
+ return {output: cargo[cargo.length - 1], inputs: cargo.slice(0, -1)}
91
+ }
92
+
93
+ export interface OwnedJob {
94
+ id: number
95
+ workshop: number
96
+ socket: number
97
+ shipId: number
98
+ coords: {x: number; y: number}
99
+ startsAt: Date
100
+ completesAt: Date
101
+ recipeId: number
102
+ quantity: number
103
+ status: JobStatus
104
+ output: CargoItem | null
105
+ inputs: CargoItem[]
106
+ /** Packed stat roll of `output`, so every job shape answers this the same way. */
107
+ outputStats?: bigint
108
+ }
package/src/shipload.ts CHANGED
@@ -12,6 +12,7 @@ import type {EpochsManager} from './managers/epochs'
12
12
  import type {ActionsManager} from './managers/actions'
13
13
  import type {ClusterManager} from './managers/cluster'
14
14
  import type {NftManager} from './managers/nft'
15
+ import type {JobsManager} from './managers/jobs'
15
16
  import type {SubscriptionsManager} from './subscriptions/manager'
16
17
  import type {GameState} from './entities/gamestate'
17
18
 
@@ -132,6 +133,10 @@ export class Shipload {
132
133
  return this._context.nft
133
134
  }
134
135
 
136
+ get jobs(): JobsManager {
137
+ return this._context.jobs
138
+ }
139
+
135
140
  get subscriptions(): SubscriptionsManager {
136
141
  return this._context.subscriptions
137
142
  }